fix conflict

This commit is contained in:
yoan 2024-11-22 11:16:54 +08:00
commit 47050769fc
83 changed files with 7524 additions and 258 deletions

View File

@ -20,10 +20,10 @@ func NewVolcengine(option *ApplyOption) Applicant {
}
func (a *volcengine) Apply() (*Certificate, error) {
access := &domain.VolcengineAccess{}
access := &domain.VolcEngineAccess{}
json.Unmarshal([]byte(a.option.Access), access)
os.Setenv("VOLC_ACCESSKEY", access.AccessKeyID)
os.Setenv("VOLC_ACCESSKEY", access.AccessKeyId)
os.Setenv("VOLC_SECRETKEY", access.SecretAccessKey)
os.Setenv("VOLC_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", a.option.Timeout))
dnsProvider, err := volcengineDns.NewDNSProvider()

View File

@ -1,21 +1,15 @@
package deployer
import (
"bytes"
"context"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"time"
"github.com/pavlo-v-chernykh/keystore-go/v4"
"github.com/pocketbase/pocketbase/models"
"software.sslmate.com/src/go-pkcs12"
"github.com/usual2970/certimate/internal/applicant"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/utils/x509"
"github.com/usual2970/certimate/internal/repository"
)
@ -34,15 +28,15 @@ const (
targetHuaweiCloudCDN = "huaweicloud-cdn"
targetHuaweiCloudELB = "huaweicloud-elb"
targetBaiduCloudCDN = "baiducloud-cdn"
targetVolcEngineLive = "volcengine-live"
targetVolcEngineCDN = "volcengine-cdn"
targetBytePlusCDN = "byteplus-cdn"
targetQiniuCdn = "qiniu-cdn"
targetDogeCloudCdn = "dogecloud-cdn"
targetLocal = "local"
targetSSH = "ssh"
targetWebhook = "webhook"
targetK8sSecret = "k8s-secret"
targetVolcengineLive = "volcengine-live"
targetVolcengineCDN = "volcengine-cdn"
targetByteplusCDN = "byteplus-cdn"
)
type DeployerOption struct {
@ -162,11 +156,11 @@ func getWithTypeAndOption(deployType string, option *DeployerOption) (Deployer,
return NewWebhookDeployer(option)
case targetK8sSecret:
return NewK8sSecretDeployer(option)
case targetVolcengineLive:
case targetVolcEngineLive:
return NewVolcengineLiveDeployer(option)
case targetVolcengineCDN:
case targetVolcEngineCDN:
return NewVolcengineCDNDeployer(option)
case targetByteplusCDN:
case targetBytePlusCDN:
return NewByteplusCDNDeployer(option)
}
return nil, errors.New("unsupported deploy target")
@ -179,57 +173,3 @@ func toStr(tag string, data any) string {
byts, _ := json.Marshal(data)
return tag + "" + string(byts)
}
func convertPEMToPFX(certificate string, privateKey string, password string) ([]byte, error) {
cert, err := x509.ParseCertificateFromPEM(certificate)
if err != nil {
return nil, err
}
privkey, err := x509.ParsePKCS1PrivateKeyFromPEM(privateKey)
if err != nil {
return nil, err
}
pfxData, err := pkcs12.LegacyRC2.Encode(privkey, cert, nil, password)
if err != nil {
return nil, err
}
return pfxData, nil
}
func convertPEMToJKS(certificate string, privateKey string, alias string, keypass string, storepass string) ([]byte, error) {
certBlock, _ := pem.Decode([]byte(certificate))
if certBlock == nil {
return nil, errors.New("failed to decode certificate PEM")
}
privkeyBlock, _ := pem.Decode([]byte(privateKey))
if privkeyBlock == nil {
return nil, errors.New("failed to decode private key PEM")
}
ks := keystore.New()
entry := keystore.PrivateKeyEntry{
CreationTime: time.Now(),
PrivateKey: privkeyBlock.Bytes,
CertificateChain: []keystore.Certificate{
{
Type: "X509",
Content: certBlock.Bytes,
},
},
}
if err := ks.SetPrivateKeyEntry(alias, entry, []byte(keypass)); err != nil {
return nil, err
}
var buf bytes.Buffer
if err := ks.Store(&buf, []byte(storepass)); err != nil {
return nil, err
}
return buf.Bytes(), nil
}

View File

@ -0,0 +1,374 @@
package deployer
import (
"encoding/json"
"fmt"
"strconv"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
providerAliyunAlb "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-alb"
providerAliyunCdn "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-cdn"
providerAliyunClb "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-clb"
providerAliyunDcdn "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-dcdn"
providerAliyunNlb "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-nlb"
providerAliyunOss "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-oss"
providerBaiduCloudCdn "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baiducloud-cdn"
providerBytePlusCdn "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/byteplus-cdn"
providerDogeCdn "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/dogecloud-cdn"
providerHuaweiCloudCdn "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/huaweicloud-cdn"
providerHuaweiCloudElb "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/huaweicloud-elb"
providerK8sSecret "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/k8s-secret"
providerLocal "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/local"
providerQiniuCdn "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/qiniu-cdn"
providerSSH "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ssh"
providerTencentCloudCdn "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-cdn"
providerTencentCloudClb "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-clb"
providerTencentCloudCos "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-cos"
providerTencentCloudEcdn "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-ecdn"
providerTencentCloudTeo "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-teo"
providerVolcEngineCdn "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-cdn"
providerVolcEngineLive "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-live"
providerWebhook "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/webhook"
"github.com/usual2970/certimate/internal/pkg/utils/maps"
)
// TODO: 该方法目前未实际使用,将在后续迭代中替换
func createDeployer(target string, accessConfig string, deployConfig map[string]any) (deployer.Deployer, deployer.Logger, error) {
logger := deployer.NewDefaultLogger()
switch target {
case targetAliyunALB, targetAliyunCDN, targetAliyunCLB, targetAliyunDCDN, targetAliyunNLB, targetAliyunOSS:
{
access := &domain.AliyunAccess{}
if err := json.Unmarshal([]byte(accessConfig), access); err != nil {
return nil, nil, fmt.Errorf("failed to unmarshal access config: %w", err)
}
switch target {
case targetAliyunALB:
deployer, err := providerAliyunAlb.NewWithLogger(&providerAliyunAlb.AliyunALBDeployerConfig{
AccessKeyId: access.AccessKeyId,
AccessKeySecret: access.AccessKeySecret,
Region: maps.GetValueAsString(deployConfig, "region"),
ResourceType: providerAliyunAlb.DeployResourceType(maps.GetValueAsString(deployConfig, "resourceType")),
LoadbalancerId: maps.GetValueAsString(deployConfig, "loadbalancerId"),
ListenerId: maps.GetValueAsString(deployConfig, "listenerId"),
}, logger)
return deployer, logger, err
case targetAliyunCDN:
deployer, err := providerAliyunCdn.NewWithLogger(&providerAliyunCdn.AliyunCDNDeployerConfig{
AccessKeyId: access.AccessKeyId,
AccessKeySecret: access.AccessKeySecret,
Domain: maps.GetValueAsString(deployConfig, "domain"),
}, logger)
return deployer, logger, err
case targetAliyunCLB:
deployer, err := providerAliyunClb.NewWithLogger(&providerAliyunClb.AliyunCLBDeployerConfig{
AccessKeyId: access.AccessKeyId,
AccessKeySecret: access.AccessKeySecret,
Region: maps.GetValueAsString(deployConfig, "region"),
ResourceType: providerAliyunClb.DeployResourceType(maps.GetValueAsString(deployConfig, "resourceType")),
LoadbalancerId: maps.GetValueAsString(deployConfig, "loadbalancerId"),
ListenerPort: maps.GetValueAsInt32(deployConfig, "listenerPort"),
}, logger)
return deployer, logger, err
case targetAliyunDCDN:
deployer, err := providerAliyunDcdn.NewWithLogger(&providerAliyunDcdn.AliyunDCDNDeployerConfig{
AccessKeyId: access.AccessKeyId,
AccessKeySecret: access.AccessKeySecret,
Domain: maps.GetValueAsString(deployConfig, "domain"),
}, logger)
return deployer, logger, err
case targetAliyunNLB:
deployer, err := providerAliyunNlb.NewWithLogger(&providerAliyunNlb.AliyunNLBDeployerConfig{
AccessKeyId: access.AccessKeyId,
AccessKeySecret: access.AccessKeySecret,
Region: maps.GetValueAsString(deployConfig, "region"),
ResourceType: providerAliyunNlb.DeployResourceType(maps.GetValueAsString(deployConfig, "resourceType")),
LoadbalancerId: maps.GetValueAsString(deployConfig, "loadbalancerId"),
ListenerId: maps.GetValueAsString(deployConfig, "listenerId"),
}, logger)
return deployer, logger, err
case targetAliyunOSS:
deployer, err := providerAliyunOss.NewWithLogger(&providerAliyunOss.AliyunOSSDeployerConfig{
AccessKeyId: access.AccessKeyId,
AccessKeySecret: access.AccessKeySecret,
Region: maps.GetValueAsString(deployConfig, "region"),
Bucket: maps.GetValueAsString(deployConfig, "bucket"),
Domain: maps.GetValueAsString(deployConfig, "domain"),
}, logger)
return deployer, logger, err
default:
break
}
}
case targetBaiduCloudCDN:
{
access := &domain.BaiduCloudAccess{}
if err := json.Unmarshal([]byte(accessConfig), access); err != nil {
return nil, nil, fmt.Errorf("failed to unmarshal access config: %w", err)
}
deployer, err := providerBaiduCloudCdn.NewWithLogger(&providerBaiduCloudCdn.BaiduCloudCDNDeployerConfig{
AccessKeyId: access.AccessKeyId,
SecretAccessKey: access.SecretAccessKey,
Domain: maps.GetValueAsString(deployConfig, "domain"),
}, logger)
return deployer, logger, err
}
case targetBytePlusCDN:
{
access := &domain.ByteplusAccess{}
if err := json.Unmarshal([]byte(accessConfig), access); err != nil {
return nil, nil, fmt.Errorf("failed to unmarshal access config: %w", err)
}
deployer, err := providerBytePlusCdn.NewWithLogger(&providerBytePlusCdn.BytePlusCDNDeployerConfig{
AccessKey: access.AccessKey,
SecretKey: access.SecretKey,
Domain: maps.GetValueAsString(deployConfig, "domain"),
}, logger)
return deployer, logger, err
}
case targetDogeCloudCdn:
{
access := &domain.DogeCloudAccess{}
if err := json.Unmarshal([]byte(accessConfig), access); err != nil {
return nil, nil, fmt.Errorf("failed to unmarshal access config: %w", err)
}
deployer, err := providerDogeCdn.NewWithLogger(&providerDogeCdn.DogeCloudCDNDeployerConfig{
AccessKey: access.AccessKey,
SecretKey: access.SecretKey,
Domain: maps.GetValueAsString(deployConfig, "domain"),
}, logger)
return deployer, logger, err
}
case targetHuaweiCloudCDN, targetHuaweiCloudELB:
{
access := &domain.HuaweiCloudAccess{}
if err := json.Unmarshal([]byte(accessConfig), access); err != nil {
return nil, nil, fmt.Errorf("failed to unmarshal access config: %w", err)
}
switch target {
case targetHuaweiCloudCDN:
deployer, err := providerHuaweiCloudCdn.NewWithLogger(&providerHuaweiCloudCdn.HuaweiCloudCDNDeployerConfig{
AccessKeyId: access.AccessKeyId,
SecretAccessKey: access.SecretAccessKey,
Region: maps.GetValueAsString(deployConfig, "region"),
Domain: maps.GetValueAsString(deployConfig, "domain"),
}, logger)
return deployer, logger, err
case targetHuaweiCloudELB:
deployer, err := providerHuaweiCloudElb.NewWithLogger(&providerHuaweiCloudElb.HuaweiCloudELBDeployerConfig{
AccessKeyId: access.AccessKeyId,
SecretAccessKey: access.SecretAccessKey,
Region: maps.GetValueAsString(deployConfig, "region"),
ResourceType: providerHuaweiCloudElb.DeployResourceType(maps.GetValueAsString(deployConfig, "resourceType")),
CertificateId: maps.GetValueAsString(deployConfig, "certificateId"),
LoadbalancerId: maps.GetValueAsString(deployConfig, "loadbalancerId"),
ListenerId: maps.GetValueAsString(deployConfig, "listenerId"),
}, logger)
return deployer, logger, err
default:
break
}
}
case targetLocal:
{
deployer, err := providerLocal.NewWithLogger(&providerLocal.LocalDeployerConfig{
ShellEnv: providerLocal.ShellEnvType(maps.GetValueAsString(deployConfig, "shellEnv")),
PreCommand: maps.GetValueAsString(deployConfig, "preCommand"),
PostCommand: maps.GetValueAsString(deployConfig, "postCommand"),
OutputFormat: providerLocal.OutputFormatType(maps.GetValueOrDefaultAsString(deployConfig, "format", "PEM")),
OutputCertPath: maps.GetValueAsString(deployConfig, "certPath"),
OutputKeyPath: maps.GetValueAsString(deployConfig, "keyPath"),
PfxPassword: maps.GetValueAsString(deployConfig, "pfxPassword"),
JksAlias: maps.GetValueAsString(deployConfig, "jksAlias"),
JksKeypass: maps.GetValueAsString(deployConfig, "jksKeypass"),
JksStorepass: maps.GetValueAsString(deployConfig, "jksStorepass"),
}, logger)
return deployer, logger, err
}
case targetK8sSecret:
{
access := &domain.KubernetesAccess{}
if err := json.Unmarshal([]byte(accessConfig), access); err != nil {
return nil, nil, fmt.Errorf("failed to unmarshal access config: %w", err)
}
deployer, err := providerK8sSecret.NewWithLogger(&providerK8sSecret.K8sSecretDeployerConfig{
KubeConfig: access.KubeConfig,
Namespace: maps.GetValueOrDefaultAsString(deployConfig, "namespace", "default"),
SecretName: maps.GetValueAsString(deployConfig, "secretName"),
SecretDataKeyForCrt: maps.GetValueOrDefaultAsString(deployConfig, "secretDataKeyForCrt", "tls.crt"),
SecretDataKeyForKey: maps.GetValueOrDefaultAsString(deployConfig, "secretDataKeyForKey", "tls.key"),
}, logger)
return deployer, logger, err
}
case targetQiniuCdn:
{
access := &domain.QiniuAccess{}
if err := json.Unmarshal([]byte(accessConfig), access); err != nil {
return nil, nil, fmt.Errorf("failed to unmarshal access config: %w", err)
}
deployer, err := providerQiniuCdn.NewWithLogger(&providerQiniuCdn.QiniuCDNDeployerConfig{
AccessKey: access.AccessKey,
SecretKey: access.SecretKey,
Domain: maps.GetValueAsString(deployConfig, "domain"),
}, logger)
return deployer, logger, err
}
case targetSSH:
{
access := &domain.SSHAccess{}
if err := json.Unmarshal([]byte(accessConfig), access); err != nil {
return nil, nil, fmt.Errorf("failed to unmarshal access config: %w", err)
}
sshPort, _ := strconv.ParseInt(access.Port, 10, 32)
deployer, err := providerSSH.NewWithLogger(&providerSSH.SshDeployerConfig{
SshHost: access.Host,
SshPort: int32(sshPort),
SshUsername: access.Username,
SshPassword: access.Password,
SshKey: access.Key,
SshKeyPassphrase: access.KeyPassphrase,
PreCommand: maps.GetValueAsString(deployConfig, "preCommand"),
PostCommand: maps.GetValueAsString(deployConfig, "postCommand"),
OutputFormat: providerSSH.OutputFormatType(maps.GetValueOrDefaultAsString(deployConfig, "format", "PEM")),
OutputCertPath: maps.GetValueAsString(deployConfig, "certPath"),
OutputKeyPath: maps.GetValueAsString(deployConfig, "keyPath"),
PfxPassword: maps.GetValueAsString(deployConfig, "pfxPassword"),
JksAlias: maps.GetValueAsString(deployConfig, "jksAlias"),
JksKeypass: maps.GetValueAsString(deployConfig, "jksKeypass"),
JksStorepass: maps.GetValueAsString(deployConfig, "jksStorepass"),
}, logger)
return deployer, logger, err
}
case targetTencentCDN, targetTencentCLB, targetTencentCOS, targetTencentECDN, targetTencentTEO:
{
access := &domain.TencentAccess{}
if err := json.Unmarshal([]byte(accessConfig), access); err != nil {
return nil, nil, fmt.Errorf("failed to unmarshal access config: %w", err)
}
switch target {
case targetTencentCDN:
deployer, err := providerTencentCloudCdn.NewWithLogger(&providerTencentCloudCdn.TencentCloudCDNDeployerConfig{
SecretId: access.SecretId,
SecretKey: access.SecretKey,
Domain: maps.GetValueAsString(deployConfig, "domain"),
}, logger)
return deployer, logger, err
case targetTencentCLB:
deployer, err := providerTencentCloudClb.NewWithLogger(&providerTencentCloudClb.TencentCloudCLBDeployerConfig{
SecretId: access.SecretId,
SecretKey: access.SecretKey,
Region: maps.GetValueAsString(deployConfig, "region"),
ResourceType: providerTencentCloudClb.DeployResourceType(maps.GetValueAsString(deployConfig, "resourceType")),
LoadbalancerId: maps.GetValueAsString(deployConfig, "loadbalancerId"),
ListenerId: maps.GetValueAsString(deployConfig, "listenerId"),
Domain: maps.GetValueAsString(deployConfig, "domain"),
}, logger)
return deployer, logger, err
case targetTencentCOS:
deployer, err := providerTencentCloudCos.NewWithLogger(&providerTencentCloudCos.TencentCloudCOSDeployerConfig{
SecretId: access.SecretId,
SecretKey: access.SecretKey,
Region: maps.GetValueAsString(deployConfig, "region"),
Bucket: maps.GetValueAsString(deployConfig, "bucket"),
Domain: maps.GetValueAsString(deployConfig, "domain"),
}, logger)
return deployer, logger, err
case targetTencentECDN:
deployer, err := providerTencentCloudEcdn.NewWithLogger(&providerTencentCloudEcdn.TencentCloudECDNDeployerConfig{
SecretId: access.SecretId,
SecretKey: access.SecretKey,
Domain: maps.GetValueAsString(deployConfig, "domain"),
}, logger)
return deployer, logger, err
case targetTencentTEO:
deployer, err := providerTencentCloudTeo.NewWithLogger(&providerTencentCloudTeo.TencentCloudTEODeployerConfig{
SecretId: access.SecretId,
SecretKey: access.SecretKey,
ZoneId: maps.GetValueAsString(deployConfig, "zoneId"),
Domain: maps.GetValueAsString(deployConfig, "domain"),
}, logger)
return deployer, logger, err
default:
break
}
}
case targetVolcEngineCDN, targetVolcEngineLive:
{
access := &domain.VolcEngineAccess{}
if err := json.Unmarshal([]byte(accessConfig), access); err != nil {
return nil, nil, fmt.Errorf("failed to unmarshal access config: %w", err)
}
switch target {
case targetVolcEngineCDN:
deployer, err := providerVolcEngineCdn.NewWithLogger(&providerVolcEngineCdn.VolcEngineCDNDeployerConfig{
AccessKey: access.AccessKey,
SecretKey: access.SecretKey,
Domain: maps.GetValueAsString(deployConfig, "domain"),
}, logger)
return deployer, logger, err
case targetVolcEngineLive:
deployer, err := providerVolcEngineLive.NewWithLogger(&providerVolcEngineLive.VolcEngineLiveDeployerConfig{
AccessKey: access.AccessKey,
SecretKey: access.SecretKey,
Domain: maps.GetValueAsString(deployConfig, "domain"),
}, logger)
return deployer, logger, err
default:
break
}
}
case targetWebhook:
{
access := &domain.WebhookAccess{}
if err := json.Unmarshal([]byte(accessConfig), access); err != nil {
return nil, nil, fmt.Errorf("failed to unmarshal access config: %w", err)
}
deployer, err := providerWebhook.NewWithLogger(&providerWebhook.WebhookDeployerConfig{
Url: access.Url,
Variables: nil, // TODO: 尚未实现
}, logger)
return deployer, logger, err
}
}
return nil, nil, fmt.Errorf("unsupported deployer target: %s", target)
}

View File

@ -5,8 +5,6 @@ import (
"encoding/json"
"errors"
"fmt"
"sort"
"strings"
"github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/basic"
"github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/global"
@ -17,6 +15,7 @@ import (
hcIamModel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3/model"
hcIamRegion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3/region"
xerrors "github.com/pkg/errors"
"golang.org/x/exp/slices"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
@ -163,9 +162,6 @@ func (u *HuaweiCloudELBDeployer) getSdkProjectId(accessKeyId, secretAccessKey, r
}
client := hcIam.NewIamClient(hcClient)
if err != nil {
return "", err
}
request := &hcIamModel.KeystoneListProjectsRequest{
Name: &region,
@ -352,11 +348,7 @@ func (d *HuaweiCloudELBDeployer) modifyListenerCertificate(ctx context.Context,
newCertificate := showNewCertificateResp.Certificate
if oldCertificate.SubjectAlternativeNames != nil && newCertificate.SubjectAlternativeNames != nil {
oldCertificateSans := oldCertificate.SubjectAlternativeNames
newCertificateSans := newCertificate.SubjectAlternativeNames
sort.Strings(*oldCertificateSans)
sort.Strings(*newCertificateSans)
if strings.Join(*oldCertificateSans, ";") == strings.Join(*newCertificateSans, ";") {
if slices.Equal(*oldCertificate.SubjectAlternativeNames, *newCertificate.SubjectAlternativeNames) {
continue
}
} else {

View File

@ -11,6 +11,7 @@ import (
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/utils/fs"
"github.com/usual2970/certimate/internal/pkg/utils/x509"
)
type LocalDeployer struct {
@ -73,7 +74,7 @@ func (d *LocalDeployer) Deploy(ctx context.Context) error {
d.infos = append(d.infos, toStr("保存私钥成功", nil))
case certFormatPFX:
pfxData, err := convertPEMToPFX(
pfxData, err := x509.TransformCertificateFromPEMToPFX(
d.option.Certificate.Certificate,
d.option.Certificate.PrivateKey,
d.option.DeployConfig.GetConfigAsString("pfxPassword"),
@ -89,7 +90,7 @@ func (d *LocalDeployer) Deploy(ctx context.Context) error {
d.infos = append(d.infos, toStr("保存证书成功", nil))
case certFormatJKS:
jksData, err := convertPEMToJKS(
jksData, err := x509.TransformCertificateFromPEMToJKS(
d.option.Certificate.Certificate,
d.option.Certificate.PrivateKey,
d.option.DeployConfig.GetConfigAsString("jksAlias"),

View File

@ -14,6 +14,7 @@ import (
"golang.org/x/crypto/ssh"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/utils/x509"
)
type SSHDeployer struct {
@ -78,7 +79,7 @@ func (d *SSHDeployer) Deploy(ctx context.Context) error {
d.infos = append(d.infos, toStr("SSH 上传私钥成功", nil))
case certFormatPFX:
pfxData, err := convertPEMToPFX(
pfxData, err := x509.TransformCertificateFromPEMToPFX(
d.option.Certificate.Certificate,
d.option.Certificate.PrivateKey,
d.option.DeployConfig.GetConfigAsString("pfxPassword"),
@ -94,7 +95,7 @@ func (d *SSHDeployer) Deploy(ctx context.Context) error {
d.infos = append(d.infos, toStr("SSH 上传证书成功", nil))
case certFormatJKS:
jksData, err := convertPEMToJKS(
jksData, err := x509.TransformCertificateFromPEMToJKS(
d.option.Certificate.Certificate,
d.option.Certificate.PrivateKey,
d.option.DeployConfig.GetConfigAsString("jksAlias"),

View File

@ -22,15 +22,15 @@ type VolcengineCDNDeployer struct {
}
func NewVolcengineCDNDeployer(option *DeployerOption) (Deployer, error) {
access := &domain.VolcengineAccess{}
access := &domain.VolcEngineAccess{}
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
return nil, xerrors.Wrap(err, "failed to get access")
}
client := cdn.NewInstance()
client.Client.SetAccessKey(access.AccessKeyID)
client.Client.SetAccessKey(access.AccessKeyId)
client.Client.SetSecretKey(access.SecretAccessKey)
uploader, err := volcenginecdn.New(&volcenginecdn.VolcengineCDNUploaderConfig{
AccessKeyId: access.AccessKeyID,
uploader, err := volcenginecdn.New(&volcenginecdn.VolcEngineCDNUploaderConfig{
AccessKeyId: access.AccessKeyId,
AccessKeySecret: access.SecretAccessKey,
})
if err != nil {

View File

@ -24,17 +24,17 @@ type VolcengineLiveDeployer struct {
}
func NewVolcengineLiveDeployer(option *DeployerOption) (Deployer, error) {
access := &domain.VolcengineAccess{}
access := &domain.VolcEngineAccess{}
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
return nil, xerrors.Wrap(err, "failed to get access")
}
client := live.NewInstance()
client.SetCredential(base.Credentials{
AccessKeyID: access.AccessKeyID,
AccessKeyID: access.AccessKeyId,
SecretAccessKey: access.SecretAccessKey,
})
uploader, err := volcenginelive.New(&volcenginelive.VolcengineLiveUploaderConfig{
AccessKeyId: access.AccessKeyID,
uploader, err := volcenginelive.New(&volcenginelive.VolcEngineLiveUploaderConfig{
AccessKeyId: access.AccessKeyId,
AccessKeySecret: access.SecretAccessKey,
})
if err != nil {

View File

@ -27,8 +27,8 @@ type AliyunAccess struct {
}
type ByteplusAccess struct {
AccessKey string
SecretKey string
AccessKey string `json:"accessKey"`
SecretKey string `json:"secretKey"`
}
type TencentAccess struct {
@ -48,9 +48,9 @@ type BaiduCloudAccess struct {
}
type AwsAccess struct {
Region string `json:"region"`
AccessKeyId string `json:"accessKeyId"`
SecretAccessKey string `json:"secretAccessKey"`
Region string `json:"region"`
HostedZoneId string `json:"hostedZoneId"`
}
@ -82,9 +82,14 @@ type PdnsAccess struct {
ApiKey string `json:"apiKey"`
}
type VolcengineAccess struct {
AccessKeyID string
SecretAccessKey string
type VolcEngineAccess struct {
AccessKey string `json:"accessKey"`
SecretKey string `json:"secretKey"`
// Deprecated: Use [AccessKey] and [SecretKey] instead in the future
AccessKeyId string `json:"accessKeyId"`
// Deprecated: Use [AccessKey] and [SecretKey] instead in the future
SecretAccessKey string `json:"secretAccessKey"`
}
type HttpreqAccess struct {

View File

@ -41,7 +41,7 @@ func (dc *DeployConfig) GetConfigAsString(key string) string {
// - defaultValue: 默认值。
//
// 出参:
// - 配置项的值。如果配置项不存在或者类型不是字符串,则返回默认值。
// - 配置项的值。如果配置项不存在、类型不是字符串或者值为零值,则返回默认值。
func (dc *DeployConfig) GetConfigOrDefaultAsString(key string, defaultValue string) string {
return maps.GetValueOrDefaultAsString(dc.Config, key, defaultValue)
}
@ -64,7 +64,7 @@ func (dc *DeployConfig) GetConfigAsInt32(key string) int32 {
// - defaultValue: 默认值。
//
// 出参:
// - 配置项的值。如果配置项不存在或者类型不是 32 位整数,则返回默认值。
// - 配置项的值。如果配置项不存在、类型不是 32 位整数或者值为零值,则返回默认值。
func (dc *DeployConfig) GetConfigOrDefaultAsInt32(key string, defaultValue int32) int32 {
return maps.GetValueOrDefaultAsInt32(dc.Config, key, defaultValue)
}

View File

@ -1,24 +1,24 @@
package notify
import (
"errors"
"fmt"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
notifierBark "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/bark"
notifierDingTalk "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/dingtalk"
notifierEmail "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/email"
notifierLark "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/lark"
notifierServerChan "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/serverchan"
notifierTelegram "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/telegram"
notifierWebhook "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/webhook"
providerBark "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/bark"
providerDingTalk "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/dingtalk"
providerEmail "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/email"
providerLark "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/lark"
providerServerChan "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/serverchan"
providerTelegram "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/telegram"
providerWebhook "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/webhook"
"github.com/usual2970/certimate/internal/pkg/utils/maps"
)
func createNotifier(channel string, channelConfig map[string]any) (notifier.Notifier, error) {
switch channel {
case domain.NotifyChannelEmail:
return notifierEmail.New(&notifierEmail.EmailNotifierConfig{
return providerEmail.New(&providerEmail.EmailNotifierConfig{
SmtpHost: maps.GetValueAsString(channelConfig, "smtpHost"),
SmtpPort: maps.GetValueAsInt32(channelConfig, "smtpPort"),
SmtpTLS: maps.GetValueOrDefaultAsBool(channelConfig, "smtpTLS", true),
@ -29,38 +29,38 @@ func createNotifier(channel string, channelConfig map[string]any) (notifier.Noti
})
case domain.NotifyChannelWebhook:
return notifierWebhook.New(&notifierWebhook.WebhookNotifierConfig{
return providerWebhook.New(&providerWebhook.WebhookNotifierConfig{
Url: maps.GetValueAsString(channelConfig, "url"),
})
case domain.NotifyChannelDingtalk:
return notifierDingTalk.New(&notifierDingTalk.DingTalkNotifierConfig{
return providerDingTalk.New(&providerDingTalk.DingTalkNotifierConfig{
AccessToken: maps.GetValueAsString(channelConfig, "accessToken"),
Secret: maps.GetValueAsString(channelConfig, "secret"),
})
case domain.NotifyChannelLark:
return notifierLark.New(&notifierLark.LarkNotifierConfig{
return providerLark.New(&providerLark.LarkNotifierConfig{
WebhookUrl: maps.GetValueAsString(channelConfig, "webhookUrl"),
})
case domain.NotifyChannelTelegram:
return notifierTelegram.New(&notifierTelegram.TelegramNotifierConfig{
return providerTelegram.New(&providerTelegram.TelegramNotifierConfig{
ApiToken: maps.GetValueAsString(channelConfig, "apiToken"),
ChatId: maps.GetValueAsInt64(channelConfig, "chatId"),
})
case domain.NotifyChannelServerChan:
return notifierServerChan.New(&notifierServerChan.ServerChanNotifierConfig{
return providerServerChan.New(&providerServerChan.ServerChanNotifierConfig{
Url: maps.GetValueAsString(channelConfig, "url"),
})
case domain.NotifyChannelBark:
return notifierBark.New(&notifierBark.BarkNotifierConfig{
return providerBark.New(&providerBark.BarkNotifierConfig{
DeviceKey: maps.GetValueAsString(channelConfig, "deviceKey"),
ServerUrl: maps.GetValueAsString(channelConfig, "serverUrl"),
})
}
return nil, errors.New("unsupported notifier channel")
return nil, fmt.Errorf("unsupported notifier channel: %s", channelConfig)
}

View File

@ -0,0 +1,24 @@
package deployer
import "context"
// 表示定义证书部署器的抽象类型接口。
// 注意与 `Uploader` 区分,“部署”通常为“上传”的后置操作。
type Deployer interface {
// 部署证书。
//
// 入参:
// - ctx上下文。
// - certPem证书 PEM 内容。
// - privkeyPem私钥 PEM 内容。
//
// 出参:
// - res部署结果。
// - err: 错误。
Deploy(ctx context.Context, certPem string, privkeyPem string) (res *DeployResult, err error)
}
// 表示证书部署结果的数据结构。
type DeployResult struct {
DeploymentData map[string]any `json:"deploymentData,omitempty"`
}

View File

@ -0,0 +1,117 @@
package deployer
import (
"encoding/json"
"fmt"
"reflect"
"strings"
)
// 表示定义证书部署器的日志记录器的抽象类型接口。
type Logger interface {
// 追加一条日志记录。
// 该方法会将 `data` 以 JSON 序列化后拼接到 `tag` 结尾。
//
// 入参:
// - tag标签。
// - data数据。
Logt(tag string, data ...any)
// 追加一条日志记录。
// 该方法会将 `args` 以 `format` 格式化。
//
// 入参:
// - format格式化字符串。
// - args格式化参数。
Logf(format string, args ...any)
// 获取所有日志记录。
GetRecords() []string
// 清空所有日志记录。
FlushRecords()
}
// 表示默认的日志记录器类型。
type DefaultLogger struct {
records []string
}
var _ Logger = (*DefaultLogger)(nil)
func (l *DefaultLogger) Logt(tag string, data ...any) {
l.ensureInitialized()
temp := make([]string, len(data)+1)
temp[0] = tag
for i, v := range data {
s := ""
if v == nil {
s = "<nil>"
} else {
switch reflect.ValueOf(v).Kind() {
case reflect.String:
s = v.(string)
case reflect.Bool,
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
reflect.Float32, reflect.Float64:
s = fmt.Sprintf("%v", v)
default:
jsonData, _ := json.Marshal(v)
s = string(jsonData)
}
}
temp[i+1] = s
}
l.records = append(l.records, strings.Join(temp, ": "))
}
func (l *DefaultLogger) Logf(format string, args ...any) {
l.ensureInitialized()
l.records = append(l.records, fmt.Sprintf(format, args...))
}
func (l *DefaultLogger) GetRecords() []string {
l.ensureInitialized()
temp := make([]string, len(l.records))
copy(temp, l.records)
return temp
}
func (l *DefaultLogger) FlushRecords() {
l.records = make([]string, 0)
}
func (l *DefaultLogger) ensureInitialized() {
if l.records == nil {
l.records = make([]string, 0)
}
}
func NewDefaultLogger() *DefaultLogger {
return &DefaultLogger{
records: make([]string, 0),
}
}
// 表示空的日志记录器类型。
// 该日志记录器不会执行任何操作。
type NilLogger struct{}
var _ Logger = (*NilLogger)(nil)
func (l *NilLogger) Logt(string, ...any) {}
func (l *NilLogger) Logf(string, ...any) {}
func (l *NilLogger) GetRecords() []string {
return make([]string, 0)
}
func (l *NilLogger) FlushRecords() {}
func NewNilLogger() *NilLogger {
return &NilLogger{}
}

View File

@ -0,0 +1,56 @@
package deployer_test
import (
"testing"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
)
/*
Shell command to run this test:
go test -v logger_test.go
*/
func TestLogger(t *testing.T) {
t.Run("Logger_Appendt", func(t *testing.T) {
logger := deployer.NewDefaultLogger()
logger.Logt("test")
logger.Logt("test_nil", nil)
logger.Logt("test_int", 1024)
logger.Logt("test_string", "certimate")
logger.Logt("test_map", map[string]interface{}{"key": "value"})
logger.Logt("test_struct", struct{ Name string }{Name: "certimate"})
logger.Logt("test_slice", []string{"certimate"})
t.Log(logger.GetRecords())
if len(logger.GetRecords()) != 7 {
t.Errorf("expected 7 records, got %d", len(logger.GetRecords()))
}
logger.FlushRecords()
if len(logger.GetRecords()) != 0 {
t.Errorf("expected 0 records, got %d", len(logger.GetRecords()))
}
})
t.Run("Logger_Appendf", func(t *testing.T) {
logger := deployer.NewDefaultLogger()
logger.Logf("test")
logger.Logf("test_nil: %v", nil)
logger.Logf("test_int: %v", 1024)
logger.Logf("test_string: %v", "certimate")
logger.Logf("test_map: %v", map[string]interface{}{"key": "value"})
logger.Logf("test_struct: %v", struct{ Name string }{Name: "certimate"})
logger.Logf("test_slice: %v", []string{"certimate"})
t.Log(logger.GetRecords())
if len(logger.GetRecords()) != 7 {
t.Errorf("expected 7 records, got %d", len(logger.GetRecords()))
}
logger.FlushRecords()
if len(logger.GetRecords()) != 0 {
t.Errorf("expected 0 records, got %d", len(logger.GetRecords()))
}
})
}

View File

@ -0,0 +1,289 @@
package aliyunalb
import (
"context"
"errors"
"fmt"
"strings"
aliyunAlb "github.com/alibabacloud-go/alb-20200616/v2/client"
aliyunOpen "github.com/alibabacloud-go/darabonba-openapi/v2/client"
"github.com/alibabacloud-go/tea/tea"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
providerCas "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/aliyun-cas"
)
type AliyunALBDeployerConfig struct {
// 阿里云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 阿里云 AccessKeySecret。
AccessKeySecret string `json:"accessKeySecret"`
// 阿里云地域。
Region string `json:"region"`
// 部署资源类型。
ResourceType DeployResourceType `json:"resourceType"`
// 负载均衡实例 ID。
// 部署资源类型为 [DEPLOY_RESOURCE_LOADBALANCER] 时必填。
LoadbalancerId string `json:"loadbalancerId,omitempty"`
// 负载均衡监听 ID。
// 部署资源类型为 [DEPLOY_RESOURCE_LISTENER] 时必填。
ListenerId string `json:"listenerId,omitempty"`
}
type AliyunALBDeployer struct {
config *AliyunALBDeployerConfig
logger deployer.Logger
sdkClient *aliyunAlb.Client
sslUploader uploader.Uploader
}
var _ deployer.Deployer = (*AliyunALBDeployer)(nil)
func New(config *AliyunALBDeployerConfig) (*AliyunALBDeployer, error) {
return NewWithLogger(config, deployer.NewNilLogger())
}
func NewWithLogger(config *AliyunALBDeployerConfig, logger deployer.Logger) (*AliyunALBDeployer, error) {
if config == nil {
return nil, errors.New("config is nil")
}
if logger == nil {
return nil, errors.New("logger is nil")
}
client, err := createSdkClient(config.AccessKeyId, config.AccessKeySecret, config.Region)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
aliyunCasRegion := config.Region
if aliyunCasRegion != "" {
// 阿里云 CAS 服务接入点是独立于 ALB 服务的
// 国内版固定接入点:华东一杭州
// 国际版固定接入点:亚太东南一新加坡
if !strings.HasPrefix(aliyunCasRegion, "cn-") {
aliyunCasRegion = "ap-southeast-1"
} else {
aliyunCasRegion = "cn-hangzhou"
}
}
uploader, err := providerCas.New(&providerCas.AliyunCASUploaderConfig{
AccessKeyId: config.AccessKeyId,
AccessKeySecret: config.AccessKeySecret,
Region: aliyunCasRegion,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &AliyunALBDeployer{
logger: logger,
config: config,
sdkClient: client,
sslUploader: uploader,
}, nil
}
func (d *AliyunALBDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
// 上传证书到 CAS
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
if err != nil {
return nil, xerrors.Wrap(err, "failed to upload certificate file")
}
d.logger.Logt("certificate file uploaded", upres)
// 根据部署资源类型决定部署方式
switch d.config.ResourceType {
case DEPLOY_RESOURCE_LOADBALANCER:
if err := d.deployToLoadbalancer(ctx, upres.CertId); err != nil {
return nil, err
}
case DEPLOY_RESOURCE_LISTENER:
if err := d.deployToListener(ctx, upres.CertId); err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unsupported resource type: %s", d.config.ResourceType)
}
return &deployer.DeployResult{}, nil
}
func (d *AliyunALBDeployer) deployToLoadbalancer(ctx context.Context, cloudCertId string) error {
if d.config.LoadbalancerId == "" {
return errors.New("config `loadbalancerId` is required")
}
listenerIds := make([]string, 0)
// 查询负载均衡实例的详细信息
// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-getloadbalancerattribute
getLoadBalancerAttributeReq := &aliyunAlb.GetLoadBalancerAttributeRequest{
LoadBalancerId: tea.String(d.config.LoadbalancerId),
}
getLoadBalancerAttributeResp, err := d.sdkClient.GetLoadBalancerAttribute(getLoadBalancerAttributeReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'alb.GetLoadBalancerAttribute'")
}
d.logger.Logt("已查询到 ALB 负载均衡实例", getLoadBalancerAttributeResp)
// 查询 HTTPS 监听列表
// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-listlisteners
listListenersPage := 1
listListenersLimit := int32(100)
var listListenersToken *string = nil
for {
listListenersReq := &aliyunAlb.ListListenersRequest{
MaxResults: tea.Int32(listListenersLimit),
NextToken: listListenersToken,
LoadBalancerIds: []*string{tea.String(d.config.LoadbalancerId)},
ListenerProtocol: tea.String("HTTPS"),
}
listListenersResp, err := d.sdkClient.ListListeners(listListenersReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'alb.ListListeners'")
}
if listListenersResp.Body.Listeners != nil {
for _, listener := range listListenersResp.Body.Listeners {
listenerIds = append(listenerIds, *listener.ListenerId)
}
}
if len(listListenersResp.Body.Listeners) == 0 || listListenersResp.Body.NextToken == nil {
break
} else {
listListenersToken = listListenersResp.Body.NextToken
listListenersPage += 1
}
}
d.logger.Logt("已查询到 ALB 负载均衡实例下的全部 HTTPS 监听", listenerIds)
// 查询 QUIC 监听列表
// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-listlisteners
listListenersPage = 1
listListenersToken = nil
for {
listListenersReq := &aliyunAlb.ListListenersRequest{
MaxResults: tea.Int32(listListenersLimit),
NextToken: listListenersToken,
LoadBalancerIds: []*string{tea.String(d.config.LoadbalancerId)},
ListenerProtocol: tea.String("QUIC"),
}
listListenersResp, err := d.sdkClient.ListListeners(listListenersReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'alb.ListListeners'")
}
if listListenersResp.Body.Listeners != nil {
for _, listener := range listListenersResp.Body.Listeners {
listenerIds = append(listenerIds, *listener.ListenerId)
}
}
if len(listListenersResp.Body.Listeners) == 0 || listListenersResp.Body.NextToken == nil {
break
} else {
listListenersToken = listListenersResp.Body.NextToken
listListenersPage += 1
}
}
d.logger.Logt("已查询到 ALB 负载均衡实例下的全部 QUIC 监听", listenerIds)
// 批量更新监听证书
var errs []error
for _, listenerId := range listenerIds {
if err := d.updateListenerCertificate(ctx, listenerId, cloudCertId); err != nil {
errs = append(errs, err)
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
func (d *AliyunALBDeployer) deployToListener(ctx context.Context, cloudCertId string) error {
if d.config.ListenerId == "" {
return errors.New("config `listenerId` is required")
}
// 更新监听
if err := d.updateListenerCertificate(ctx, d.config.ListenerId, cloudCertId); err != nil {
return err
}
return nil
}
func (d *AliyunALBDeployer) updateListenerCertificate(ctx context.Context, cloudListenerId string, cloudCertId string) error {
// 查询监听的属性
// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-getlistenerattribute
getListenerAttributeReq := &aliyunAlb.GetListenerAttributeRequest{
ListenerId: tea.String(cloudListenerId),
}
getListenerAttributeResp, err := d.sdkClient.GetListenerAttribute(getListenerAttributeReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'alb.GetListenerAttribute'")
}
d.logger.Logt("已查询到 ALB 监听配置", getListenerAttributeResp)
// 修改监听的属性
// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-updatelistenerattribute
updateListenerAttributeReq := &aliyunAlb.UpdateListenerAttributeRequest{
ListenerId: tea.String(cloudListenerId),
Certificates: []*aliyunAlb.UpdateListenerAttributeRequestCertificates{{
CertificateId: tea.String(cloudCertId),
}},
}
updateListenerAttributeResp, err := d.sdkClient.UpdateListenerAttribute(updateListenerAttributeReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'alb.UpdateListenerAttribute'")
}
d.logger.Logt("已更新 ALB 监听配置", updateListenerAttributeResp)
// TODO: #347
return nil
}
func createSdkClient(accessKeyId, accessKeySecret, region string) (*aliyunAlb.Client, error) {
if region == "" {
region = "cn-hangzhou"
}
// 接入点一览 https://www.alibabacloud.com/help/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-endpoint
var endpoint string
switch region {
case "cn-hangzhou-finance":
endpoint = "alb.cn-hangzhou.aliyuncs.com"
default:
endpoint = fmt.Sprintf("alb.%s.aliyuncs.com", region)
}
config := &aliyunOpen.Config{
AccessKeyId: tea.String(accessKeyId),
AccessKeySecret: tea.String(accessKeySecret),
Endpoint: tea.String(endpoint),
}
client, err := aliyunAlb.NewClient(config)
if err != nil {
return nil, err
}
return client, nil
}

View File

@ -0,0 +1,118 @@
package aliyunalb_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-alb"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fAccessKeySecret string
fRegion string
fLoadbalancerId string
fListenerId string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_ALIYUNALB_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "")
flag.StringVar(&fRegion, argsPrefix+"REGION", "", "")
flag.StringVar(&fLoadbalancerId, argsPrefix+"LOADBALANCERID", "", "")
flag.StringVar(&fListenerId, argsPrefix+"LISTENERID", "", "")
}
/*
Shell command to run this test:
go test -v aliyun_alb_test.go -args \
--CERTIMATE_DEPLOYER_ALIYUNALB_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_ALIYUNALB_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_ALIYUNALB_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_DEPLOYER_ALIYUNALB_ACCESSKEYSECRET="your-access-key-secret" \
--CERTIMATE_DEPLOYER_ALIYUNALB_REGION="cn-hangzhou" \
--CERTIMATE_DEPLOYER_ALIYUNALB_LOADBALANCERID="your-alb-instance-id" \
--CERTIMATE_DEPLOYER_ALIYUNALB_LISTENERID="your-alb-listener-id"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy_ToLoadbalancer", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret),
fmt.Sprintf("REGION: %v", fRegion),
fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId),
}, "\n"))
deployer, err := provider.New(&provider.AliyunALBDeployerConfig{
AccessKeyId: fAccessKeyId,
AccessKeySecret: fAccessKeySecret,
Region: fRegion,
ResourceType: provider.DEPLOY_RESOURCE_LOADBALANCER,
LoadbalancerId: fLoadbalancerId,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
t.Run("Deploy_ToListener", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret),
fmt.Sprintf("REGION: %v", fRegion),
fmt.Sprintf("LISTENERID: %v", fListenerId),
}, "\n"))
deployer, err := provider.New(&provider.AliyunALBDeployerConfig{
AccessKeyId: fAccessKeyId,
AccessKeySecret: fAccessKeySecret,
Region: fRegion,
ResourceType: provider.DEPLOY_RESOURCE_LISTENER,
ListenerId: fListenerId,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

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

View File

@ -0,0 +1,93 @@
package aliyuncdn
import (
"context"
"errors"
"fmt"
"time"
aliyunCdn "github.com/alibabacloud-go/cdn-20180510/v5/client"
aliyunOpen "github.com/alibabacloud-go/darabonba-openapi/v2/client"
"github.com/alibabacloud-go/tea/tea"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
)
type AliyunCDNDeployerConfig struct {
// 阿里云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 阿里云 AccessKeySecret。
AccessKeySecret string `json:"accessKeySecret"`
// 加速域名(不支持泛域名)。
Domain string `json:"domain"`
}
type AliyunCDNDeployer struct {
config *AliyunCDNDeployerConfig
logger deployer.Logger
sdkClient *aliyunCdn.Client
}
var _ deployer.Deployer = (*AliyunCDNDeployer)(nil)
func New(config *AliyunCDNDeployerConfig) (*AliyunCDNDeployer, error) {
return NewWithLogger(config, deployer.NewNilLogger())
}
func NewWithLogger(config *AliyunCDNDeployerConfig, logger deployer.Logger) (*AliyunCDNDeployer, error) {
if config == nil {
return nil, errors.New("config is nil")
}
if logger == nil {
return nil, errors.New("logger is nil")
}
client, err := createSdkClient(config.AccessKeyId, config.AccessKeySecret)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
return &AliyunCDNDeployer{
logger: logger,
config: config,
sdkClient: client,
}, nil
}
func (d *AliyunCDNDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
// 设置 CDN 域名域名证书
// REF: https://help.aliyun.com/zh/cdn/developer-reference/api-cdn-2018-05-10-setcdndomainsslcertificate
setCdnDomainSSLCertificateReq := &aliyunCdn.SetCdnDomainSSLCertificateRequest{
DomainName: tea.String(d.config.Domain),
CertName: tea.String(fmt.Sprintf("certimate-%d", time.Now().UnixMilli())),
CertType: tea.String("upload"),
SSLProtocol: tea.String("on"),
SSLPub: tea.String(certPem),
SSLPri: tea.String(privkeyPem),
}
setCdnDomainSSLCertificateResp, err := d.sdkClient.SetCdnDomainSSLCertificate(setCdnDomainSSLCertificateReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.SetCdnDomainSSLCertificate'")
}
d.logger.Logt("已设置 CDN 域名证书", setCdnDomainSSLCertificateResp)
return &deployer.DeployResult{}, nil
}
func createSdkClient(accessKeyId, accessKeySecret string) (*aliyunCdn.Client, error) {
config := &aliyunOpen.Config{
AccessKeyId: tea.String(accessKeyId),
AccessKeySecret: tea.String(accessKeySecret),
Endpoint: tea.String("cdn.aliyuncs.com"),
}
client, err := aliyunCdn.NewClient(config)
if err != nil {
return nil, err
}
return client, nil
}

View File

@ -0,0 +1,75 @@
package aliyuncdn_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-cdn"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fAccessKeySecret string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_ALIYUNCDN_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v aliyun_cdn_test.go -args \
--CERTIMATE_DEPLOYER_ALIYUNCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_ALIYUNCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_ALIYUNCDN_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_DEPLOYER_ALIYUNCDN_ACCESSKEYSECRET="your-access-key-secret" \
--CERTIMATE_DEPLOYER_ALIYUNCDN_DOMAIN="example.com"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.New(&provider.AliyunCDNDeployerConfig{
AccessKeyId: fAccessKeyId,
AccessKeySecret: fAccessKeySecret,
Domain: fDomain,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@ -0,0 +1,291 @@
package aliyunclb
import (
"context"
"errors"
"fmt"
aliyunOpen "github.com/alibabacloud-go/darabonba-openapi/v2/client"
aliyunSlb "github.com/alibabacloud-go/slb-20140515/v4/client"
"github.com/alibabacloud-go/tea/tea"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
providerSlb "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/aliyun-slb"
)
type AliyunCLBDeployerConfig struct {
// 阿里云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 阿里云 AccessKeySecret。
AccessKeySecret string `json:"accessKeySecret"`
// 阿里云地域。
Region string `json:"region"`
// 部署资源类型。
ResourceType DeployResourceType `json:"resourceType"`
// 负载均衡实例 ID。
// 部署资源类型为 [DEPLOY_RESOURCE_LOADBALANCER]、[DEPLOY_RESOURCE_LISTENER] 时必填。
LoadbalancerId string `json:"loadbalancerId,omitempty"`
// 负载均衡监听端口。
// 部署资源类型为 [DEPLOY_RESOURCE_LISTENER] 时必填。
ListenerPort int32 `json:"listenerPort,omitempty"`
}
type AliyunCLBDeployer struct {
config *AliyunCLBDeployerConfig
logger deployer.Logger
sdkClient *aliyunSlb.Client
sslUploader uploader.Uploader
}
var _ deployer.Deployer = (*AliyunCLBDeployer)(nil)
func New(config *AliyunCLBDeployerConfig) (*AliyunCLBDeployer, error) {
return NewWithLogger(config, deployer.NewNilLogger())
}
func NewWithLogger(config *AliyunCLBDeployerConfig, logger deployer.Logger) (*AliyunCLBDeployer, error) {
if config == nil {
return nil, errors.New("config is nil")
}
if logger == nil {
return nil, errors.New("logger is nil")
}
client, err := createSdkClient(config.AccessKeyId, config.AccessKeySecret, config.Region)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
uploader, err := providerSlb.New(&providerSlb.AliyunSLBUploaderConfig{
AccessKeyId: config.AccessKeyId,
AccessKeySecret: config.AccessKeySecret,
Region: config.Region,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &AliyunCLBDeployer{
logger: logger,
config: config,
sdkClient: client,
sslUploader: uploader,
}, nil
}
func (d *AliyunCLBDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
// 上传证书到 SLB
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
if err != nil {
return nil, xerrors.Wrap(err, "failed to upload certificate file")
}
d.logger.Logt("certificate file uploaded", upres)
// 根据部署资源类型决定部署方式
switch d.config.ResourceType {
case DEPLOY_RESOURCE_LOADBALANCER:
if err := d.deployToLoadbalancer(ctx, upres.CertId); err != nil {
return nil, err
}
case DEPLOY_RESOURCE_LISTENER:
if err := d.deployToListener(ctx, upres.CertId); err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unsupported resource type: %s", d.config.ResourceType)
}
return &deployer.DeployResult{}, nil
}
func (d *AliyunCLBDeployer) deployToLoadbalancer(ctx context.Context, cloudCertId string) error {
if d.config.LoadbalancerId == "" {
return errors.New("config `loadbalancerId` is required")
}
listenerPorts := make([]int32, 0)
// 查询负载均衡实例的详细信息
// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describeloadbalancerattribute
describeLoadBalancerAttributeReq := &aliyunSlb.DescribeLoadBalancerAttributeRequest{
RegionId: tea.String(d.config.Region),
LoadBalancerId: tea.String(d.config.LoadbalancerId),
}
describeLoadBalancerAttributeResp, err := d.sdkClient.DescribeLoadBalancerAttribute(describeLoadBalancerAttributeReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'slb.DescribeLoadBalancerAttribute'")
}
d.logger.Logt("已查询到 CLB 负载均衡实例", describeLoadBalancerAttributeResp)
// 查询 HTTPS 监听列表
// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describeloadbalancerlisteners
listListenersPage := 1
listListenersLimit := int32(100)
var listListenersToken *string = nil
for {
describeLoadBalancerListenersReq := &aliyunSlb.DescribeLoadBalancerListenersRequest{
RegionId: tea.String(d.config.Region),
MaxResults: tea.Int32(listListenersLimit),
NextToken: listListenersToken,
LoadBalancerId: []*string{tea.String(d.config.LoadbalancerId)},
ListenerProtocol: tea.String("https"),
}
describeLoadBalancerListenersResp, err := d.sdkClient.DescribeLoadBalancerListeners(describeLoadBalancerListenersReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'slb.DescribeLoadBalancerListeners'")
}
if describeLoadBalancerListenersResp.Body.Listeners != nil {
for _, listener := range describeLoadBalancerListenersResp.Body.Listeners {
listenerPorts = append(listenerPorts, *listener.ListenerPort)
}
}
if len(describeLoadBalancerListenersResp.Body.Listeners) == 0 || describeLoadBalancerListenersResp.Body.NextToken == nil {
break
} else {
listListenersToken = describeLoadBalancerListenersResp.Body.NextToken
listListenersPage += 1
}
}
d.logger.Logt("已查询到 CLB 负载均衡实例下的全部 HTTPS 监听", listenerPorts)
// 批量更新监听证书
var errs []error
for _, listenerPort := range listenerPorts {
if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, listenerPort, cloudCertId); err != nil {
errs = append(errs, err)
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
func (d *AliyunCLBDeployer) deployToListener(ctx context.Context, cloudCertId string) error {
if d.config.LoadbalancerId == "" {
return errors.New("config `loadbalancerId` is required")
}
if d.config.ListenerPort == 0 {
return errors.New("config `listenerPort` is required")
}
// 更新监听
if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, d.config.ListenerPort, cloudCertId); err != nil {
return err
}
return nil
}
func (d *AliyunCLBDeployer) updateListenerCertificate(ctx context.Context, cloudLoadbalancerId string, cloudListenerPort int32, cloudCertId string) error {
// 查询监听配置
// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describeloadbalancerhttpslistenerattribute
describeLoadBalancerHTTPSListenerAttributeReq := &aliyunSlb.DescribeLoadBalancerHTTPSListenerAttributeRequest{
LoadBalancerId: tea.String(cloudLoadbalancerId),
ListenerPort: tea.Int32(cloudListenerPort),
}
describeLoadBalancerHTTPSListenerAttributeResp, err := d.sdkClient.DescribeLoadBalancerHTTPSListenerAttribute(describeLoadBalancerHTTPSListenerAttributeReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'slb.DescribeLoadBalancerHTTPSListenerAttribute'")
}
d.logger.Logt("已查询到 CLB HTTPS 监听配置", describeLoadBalancerHTTPSListenerAttributeResp)
// 查询扩展域名
// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describedomainextensions
describeDomainExtensionsReq := &aliyunSlb.DescribeDomainExtensionsRequest{
RegionId: tea.String(d.config.Region),
LoadBalancerId: tea.String(cloudLoadbalancerId),
ListenerPort: tea.Int32(cloudListenerPort),
}
describeDomainExtensionsResp, err := d.sdkClient.DescribeDomainExtensions(describeDomainExtensionsReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'slb.DescribeDomainExtensions'")
}
d.logger.Logt("已查询到 CLB 扩展域名", describeDomainExtensionsResp)
// 遍历修改扩展域名
// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-setdomainextensionattribute
//
// 这里仅修改跟被替换证书一致的扩展域名
if describeDomainExtensionsResp.Body.DomainExtensions != nil && describeDomainExtensionsResp.Body.DomainExtensions.DomainExtension != nil {
for _, domainExtension := range describeDomainExtensionsResp.Body.DomainExtensions.DomainExtension {
if *domainExtension.ServerCertificateId != *describeLoadBalancerHTTPSListenerAttributeResp.Body.ServerCertificateId {
continue
}
setDomainExtensionAttributeReq := &aliyunSlb.SetDomainExtensionAttributeRequest{
RegionId: tea.String(d.config.Region),
DomainExtensionId: tea.String(*domainExtension.DomainExtensionId),
ServerCertificateId: tea.String(cloudCertId),
}
_, err := d.sdkClient.SetDomainExtensionAttribute(setDomainExtensionAttributeReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'slb.SetDomainExtensionAttribute'")
}
}
}
// 修改监听配置
// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-setloadbalancerhttpslistenerattribute
//
// 注意修改监听配置要放在修改扩展域名之后
setLoadBalancerHTTPSListenerAttributeReq := &aliyunSlb.SetLoadBalancerHTTPSListenerAttributeRequest{
RegionId: tea.String(d.config.Region),
LoadBalancerId: tea.String(cloudLoadbalancerId),
ListenerPort: tea.Int32(cloudListenerPort),
ServerCertificateId: tea.String(cloudCertId),
}
setLoadBalancerHTTPSListenerAttributeResp, err := d.sdkClient.SetLoadBalancerHTTPSListenerAttribute(setLoadBalancerHTTPSListenerAttributeReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'slb.SetLoadBalancerHTTPSListenerAttribute'")
}
d.logger.Logt("已更新 CLB HTTPS 监听配置", setLoadBalancerHTTPSListenerAttributeResp)
return nil
}
func createSdkClient(accessKeyId, accessKeySecret, region string) (*aliyunSlb.Client, error) {
if region == "" {
region = "cn-hangzhou" // CLB(SLB) 服务默认区域:华东一杭州
}
// 接入点一览 https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-endpoint
var endpoint string
switch region {
case
"cn-hangzhou",
"cn-hangzhou-finance",
"cn-shanghai-finance-1",
"cn-shenzhen-finance-1":
endpoint = "slb.aliyuncs.com"
default:
endpoint = fmt.Sprintf("slb.%s.aliyuncs.com", region)
}
config := &aliyunOpen.Config{
AccessKeyId: tea.String(accessKeyId),
AccessKeySecret: tea.String(accessKeySecret),
Endpoint: tea.String(endpoint),
}
client, err := aliyunSlb.NewClient(config)
if err != nil {
return nil, err
}
return client, nil
}

View File

@ -0,0 +1,120 @@
package aliyunclb_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-clb"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fAccessKeySecret string
fRegion string
fLoadbalancerId string
fListenerPort int
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_ALIYUNCLB_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "")
flag.StringVar(&fRegion, argsPrefix+"REGION", "", "")
flag.StringVar(&fLoadbalancerId, argsPrefix+"LOADBALANCERID", "", "")
flag.IntVar(&fListenerPort, argsPrefix+"LISTENERPORT", 443, "")
}
/*
Shell command to run this test:
go test -v aliyun_clb_test.go -args \
--CERTIMATE_DEPLOYER_ALIYUNCLB_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_ALIYUNCLB_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_ALIYUNCLB_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_DEPLOYER_ALIYUNCLB_ACCESSKEYSECRET="your-access-key-secret" \
--CERTIMATE_DEPLOYER_ALIYUNCLB_REGION="cn-hangzhou" \
--CERTIMATE_DEPLOYER_ALIYUNCLB_LOADBALANCERID="your-clb-instance-id" \
--CERTIMATE_DEPLOYER_ALIYUNCLB_LISTENERPORT=443
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy_ToLoadbalancer", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret),
fmt.Sprintf("REGION: %v", fRegion),
fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId),
}, "\n"))
deployer, err := provider.New(&provider.AliyunCLBDeployerConfig{
AccessKeyId: fAccessKeyId,
AccessKeySecret: fAccessKeySecret,
Region: fRegion,
ResourceType: provider.DEPLOY_RESOURCE_LOADBALANCER,
LoadbalancerId: fLoadbalancerId,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
t.Run("Deploy_ToListener", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret),
fmt.Sprintf("REGION: %v", fRegion),
fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId),
fmt.Sprintf("LISTENERPORT: %v", fListenerPort),
}, "\n"))
deployer, err := provider.New(&provider.AliyunCLBDeployerConfig{
AccessKeyId: fAccessKeyId,
AccessKeySecret: fAccessKeySecret,
Region: fRegion,
ResourceType: provider.DEPLOY_RESOURCE_LISTENER,
LoadbalancerId: fLoadbalancerId,
ListenerPort: int32(fListenerPort),
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

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

View File

@ -0,0 +1,97 @@
package aliyundcdn
import (
"context"
"errors"
"fmt"
"strings"
"time"
aliyunOpen "github.com/alibabacloud-go/darabonba-openapi/v2/client"
aliyunDcdn "github.com/alibabacloud-go/dcdn-20180115/v3/client"
"github.com/alibabacloud-go/tea/tea"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
)
type AliyunDCDNDeployerConfig struct {
// 阿里云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 阿里云 AccessKeySecret。
AccessKeySecret string `json:"accessKeySecret"`
// 加速域名(支持泛域名)。
Domain string `json:"domain"`
}
type AliyunDCDNDeployer struct {
config *AliyunDCDNDeployerConfig
logger deployer.Logger
sdkClient *aliyunDcdn.Client
}
var _ deployer.Deployer = (*AliyunDCDNDeployer)(nil)
func New(config *AliyunDCDNDeployerConfig) (*AliyunDCDNDeployer, error) {
return NewWithLogger(config, deployer.NewNilLogger())
}
func NewWithLogger(config *AliyunDCDNDeployerConfig, logger deployer.Logger) (*AliyunDCDNDeployer, error) {
if config == nil {
return nil, errors.New("config is nil")
}
if logger == nil {
return nil, errors.New("logger is nil")
}
client, err := createSdkClient(config.AccessKeyId, config.AccessKeySecret)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
return &AliyunDCDNDeployer{
logger: logger,
config: config,
sdkClient: client,
}, nil
}
func (d *AliyunDCDNDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
// "*.example.com" → ".example.com",适配阿里云 DCDN 要求的泛域名格式
domain := strings.TrimPrefix(d.config.Domain, "*")
// 配置域名证书
// REF: https://help.aliyun.com/zh/edge-security-acceleration/dcdn/developer-reference/api-dcdn-2018-01-15-setdcdndomainsslcertificate
setDcdnDomainSSLCertificateReq := &aliyunDcdn.SetDcdnDomainSSLCertificateRequest{
DomainName: tea.String(domain),
CertName: tea.String(fmt.Sprintf("certimate-%d", time.Now().UnixMilli())),
CertType: tea.String("upload"),
SSLProtocol: tea.String("on"),
SSLPub: tea.String(certPem),
SSLPri: tea.String(privkeyPem),
}
setDcdnDomainSSLCertificateResp, err := d.sdkClient.SetDcdnDomainSSLCertificate(setDcdnDomainSSLCertificateReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'dcdn.SetDcdnDomainSSLCertificate'")
}
d.logger.Logt("已配置 DCDN 域名证书", setDcdnDomainSSLCertificateResp)
return &deployer.DeployResult{}, nil
}
func createSdkClient(accessKeyId, accessKeySecret string) (*aliyunDcdn.Client, error) {
config := &aliyunOpen.Config{
AccessKeyId: tea.String(accessKeyId),
AccessKeySecret: tea.String(accessKeySecret),
Endpoint: tea.String("dcdn.aliyuncs.com"),
}
client, err := aliyunDcdn.NewClient(config)
if err != nil {
return nil, err
}
return client, nil
}

View File

@ -0,0 +1,75 @@
package aliyundcdn_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-dcdn"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fAccessKeySecret string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_ALIYUNDCDN_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v aliyun_dcdn_test.go -args \
--CERTIMATE_DEPLOYER_ALIYUNDCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_ALIYUNDCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_ALIYUNDCDN_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_DEPLOYER_ALIYUNDCDN_ACCESSKEYSECRET="your-access-key-secret" \
--CERTIMATE_DEPLOYER_ALIYUNDCDN_DOMAIN="example.com"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.New(&provider.AliyunDCDNDeployerConfig{
AccessKeyId: fAccessKeyId,
AccessKeySecret: fAccessKeySecret,
Domain: fDomain,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@ -0,0 +1,251 @@
package aliyunnlb
import (
"context"
"errors"
"fmt"
"strings"
aliyunOpen "github.com/alibabacloud-go/darabonba-openapi/v2/client"
aliyunNlb "github.com/alibabacloud-go/nlb-20220430/v2/client"
"github.com/alibabacloud-go/tea/tea"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
providerCas "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/aliyun-cas"
)
type AliyunNLBDeployerConfig struct {
// 阿里云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 阿里云 AccessKeySecret。
AccessKeySecret string `json:"accessKeySecret"`
// 阿里云地域。
Region string `json:"region"`
// 部署资源类型。
ResourceType DeployResourceType `json:"resourceType"`
// 负载均衡实例 ID。
// 部署资源类型为 [DEPLOY_RESOURCE_LOADBALANCER] 时必填。
LoadbalancerId string `json:"loadbalancerId,omitempty"`
// 负载均衡监听 ID。
// 部署资源类型为 [DEPLOY_RESOURCE_LISTENER] 时必填。
ListenerId string `json:"listenerId,omitempty"`
}
type AliyunNLBDeployer struct {
config *AliyunNLBDeployerConfig
logger deployer.Logger
sdkClient *aliyunNlb.Client
sslUploader uploader.Uploader
}
var _ deployer.Deployer = (*AliyunNLBDeployer)(nil)
func New(config *AliyunNLBDeployerConfig) (*AliyunNLBDeployer, error) {
return NewWithLogger(config, deployer.NewNilLogger())
}
func NewWithLogger(config *AliyunNLBDeployerConfig, logger deployer.Logger) (*AliyunNLBDeployer, error) {
if config == nil {
return nil, errors.New("config is nil")
}
if logger == nil {
return nil, errors.New("logger is nil")
}
client, err := createSdkClient(config.AccessKeyId, config.AccessKeySecret, config.Region)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
aliyunCasRegion := config.Region
if aliyunCasRegion != "" {
// 阿里云 CAS 服务接入点是独立于 NLB 服务的
// 国内版固定接入点:华东一杭州
// 国际版固定接入点:亚太东南一新加坡
if !strings.HasPrefix(aliyunCasRegion, "cn-") {
aliyunCasRegion = "ap-southeast-1"
} else {
aliyunCasRegion = "cn-hangzhou"
}
}
uploader, err := providerCas.New(&providerCas.AliyunCASUploaderConfig{
AccessKeyId: config.AccessKeyId,
AccessKeySecret: config.AccessKeySecret,
Region: aliyunCasRegion,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &AliyunNLBDeployer{
logger: logger,
config: config,
sdkClient: client,
sslUploader: uploader,
}, nil
}
func (d *AliyunNLBDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
// 上传证书到 CAS
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
if err != nil {
return nil, xerrors.Wrap(err, "failed to upload certificate file")
}
d.logger.Logt("certificate file uploaded", upres)
// 根据部署资源类型决定部署方式
switch d.config.ResourceType {
case DEPLOY_RESOURCE_LOADBALANCER:
if err := d.deployToLoadbalancer(ctx, upres.CertId); err != nil {
return nil, err
}
case DEPLOY_RESOURCE_LISTENER:
if err := d.deployToListener(ctx, upres.CertId); err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unsupported resource type: %s", d.config.ResourceType)
}
return &deployer.DeployResult{}, nil
}
func (d *AliyunNLBDeployer) deployToLoadbalancer(ctx context.Context, cloudCertId string) error {
if d.config.LoadbalancerId == "" {
return errors.New("config `loadbalancerId` is required")
}
listenerIds := make([]string, 0)
// 查询负载均衡实例的详细信息
// REF: https://help.aliyun.com/zh/slb/network-load-balancer/developer-reference/api-nlb-2022-04-30-getloadbalancerattribute
getLoadBalancerAttributeReq := &aliyunNlb.GetLoadBalancerAttributeRequest{
LoadBalancerId: tea.String(d.config.LoadbalancerId),
}
getLoadBalancerAttributeResp, err := d.sdkClient.GetLoadBalancerAttribute(getLoadBalancerAttributeReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'nlb.GetLoadBalancerAttribute'")
}
d.logger.Logt("已查询到 NLB 负载均衡实例", getLoadBalancerAttributeResp)
// 查询 TCPSSL 监听列表
// REF: https://help.aliyun.com/zh/slb/network-load-balancer/developer-reference/api-nlb-2022-04-30-listlisteners
listListenersPage := 1
listListenersLimit := int32(100)
var listListenersToken *string = nil
for {
listListenersReq := &aliyunNlb.ListListenersRequest{
MaxResults: tea.Int32(listListenersLimit),
NextToken: listListenersToken,
LoadBalancerIds: []*string{tea.String(d.config.LoadbalancerId)},
ListenerProtocol: tea.String("TCPSSL"),
}
listListenersResp, err := d.sdkClient.ListListeners(listListenersReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'nlb.ListListeners'")
}
if listListenersResp.Body.Listeners != nil {
for _, listener := range listListenersResp.Body.Listeners {
listenerIds = append(listenerIds, *listener.ListenerId)
}
}
if len(listListenersResp.Body.Listeners) == 0 || listListenersResp.Body.NextToken == nil {
break
} else {
listListenersToken = listListenersResp.Body.NextToken
listListenersPage += 1
}
}
d.logger.Logt("已查询到 NLB 负载均衡实例下的全部 TCPSSL 监听", listenerIds)
// 批量更新监听证书
var errs []error
for _, listenerId := range listenerIds {
if err := d.updateListenerCertificate(ctx, listenerId, cloudCertId); err != nil {
errs = append(errs, err)
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
func (d *AliyunNLBDeployer) deployToListener(ctx context.Context, cloudCertId string) error {
if d.config.ListenerId == "" {
return errors.New("config `listenerId` is required")
}
// 更新监听
if err := d.updateListenerCertificate(ctx, d.config.ListenerId, cloudCertId); err != nil {
return err
}
return nil
}
func (d *AliyunNLBDeployer) updateListenerCertificate(ctx context.Context, cloudListenerId string, cloudCertId string) error {
// 查询监听的属性
// REF: https://help.aliyun.com/zh/slb/network-load-balancer/developer-reference/api-nlb-2022-04-30-getlistenerattribute
getListenerAttributeReq := &aliyunNlb.GetListenerAttributeRequest{
ListenerId: tea.String(cloudListenerId),
}
getListenerAttributeResp, err := d.sdkClient.GetListenerAttribute(getListenerAttributeReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'nlb.GetListenerAttribute'")
}
d.logger.Logt("已查询到 NLB 监听配置", getListenerAttributeResp)
// 修改监听的属性
// REF: https://help.aliyun.com/zh/slb/network-load-balancer/developer-reference/api-nlb-2022-04-30-updatelistenerattribute
updateListenerAttributeReq := &aliyunNlb.UpdateListenerAttributeRequest{
ListenerId: tea.String(cloudListenerId),
CertificateIds: []*string{tea.String(cloudCertId)},
}
updateListenerAttributeResp, err := d.sdkClient.UpdateListenerAttribute(updateListenerAttributeReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'nlb.UpdateListenerAttribute'")
}
d.logger.Logt("已更新 NLB 监听配置", updateListenerAttributeResp)
return nil
}
func createSdkClient(accessKeyId, accessKeySecret, region string) (*aliyunNlb.Client, error) {
if region == "" {
region = "cn-hangzhou" // NLB 服务默认区域:华东一杭州
}
// 接入点一览 https://help.aliyun.com/zh/slb/network-load-balancer/developer-reference/api-nlb-2022-04-30-endpoint
var endpoint string
switch region {
default:
endpoint = fmt.Sprintf("nlb.%s.aliyuncs.com", region)
}
config := &aliyunOpen.Config{
AccessKeyId: tea.String(accessKeyId),
AccessKeySecret: tea.String(accessKeySecret),
Endpoint: tea.String(endpoint),
}
client, err := aliyunNlb.NewClient(config)
if err != nil {
return nil, err
}
return client, nil
}

View File

@ -0,0 +1,119 @@
package aliyunnlb_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-nlb"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fAccessKeySecret string
fRegion string
fLoadbalancerId string
fListenerId string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_ALIYUNNLB_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "")
flag.StringVar(&fRegion, argsPrefix+"REGION", "", "")
flag.StringVar(&fLoadbalancerId, argsPrefix+"LOADBALANCERID", "", "")
flag.StringVar(&fListenerId, argsPrefix+"LISTENERID", "", "")
}
/*
Shell command to run this test:
go test -v aliyun_nlb_test.go -args \
--CERTIMATE_DEPLOYER_ALIYUNNLB_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_ALIYUNNLB_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_ALIYUNNLB_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_DEPLOYER_ALIYUNNLB_ACCESSKEYSECRET="your-access-key-secret" \
--CERTIMATE_DEPLOYER_ALIYUNNLB_REGION="cn-hangzhou" \
--CERTIMATE_DEPLOYER_ALIYUNNLB_LOADBALANCERID="your-nlb-instance-id" \
--CERTIMATE_DEPLOYER_ALIYUNNLB_LISTENERID="your-nlb-listener-id"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy_ToLoadbalancer", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret),
fmt.Sprintf("REGION: %v", fRegion),
fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId),
}, "\n"))
deployer, err := provider.New(&provider.AliyunNLBDeployerConfig{
AccessKeyId: fAccessKeyId,
AccessKeySecret: fAccessKeySecret,
Region: fRegion,
ResourceType: provider.DEPLOY_RESOURCE_LOADBALANCER,
LoadbalancerId: fLoadbalancerId,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
t.Run("Deploy_ToListener", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret),
fmt.Sprintf("REGION: %v", fRegion),
fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId),
fmt.Sprintf("LISTENERID: %v", fListenerId),
}, "\n"))
deployer, err := provider.New(&provider.AliyunNLBDeployerConfig{
AccessKeyId: fAccessKeyId,
AccessKeySecret: fAccessKeySecret,
Region: fRegion,
ResourceType: provider.DEPLOY_RESOURCE_LISTENER,
ListenerId: fListenerId,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

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

View File

@ -0,0 +1,112 @@
package aliyunoss
import (
"context"
"errors"
"fmt"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
)
type AliyunOSSDeployerConfig struct {
// 阿里云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 阿里云 AccessKeySecret。
AccessKeySecret string `json:"accessKeySecret"`
// 阿里云地域。
Region string `json:"region"`
// 存储桶名。
Bucket string `json:"bucket"`
// 自定义域名(不支持泛域名)。
Domain string `json:"domain"`
}
type AliyunOSSDeployer struct {
config *AliyunOSSDeployerConfig
logger deployer.Logger
sdkClient *oss.Client
}
var _ deployer.Deployer = (*AliyunOSSDeployer)(nil)
func New(config *AliyunOSSDeployerConfig) (*AliyunOSSDeployer, error) {
return NewWithLogger(config, deployer.NewNilLogger())
}
func NewWithLogger(config *AliyunOSSDeployerConfig, logger deployer.Logger) (*AliyunOSSDeployer, error) {
if config == nil {
return nil, errors.New("config is nil")
}
if logger == nil {
return nil, errors.New("logger is nil")
}
client, err := createSdkClient(config.AccessKeyId, config.AccessKeySecret, config.Region)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
return &AliyunOSSDeployer{
logger: logger,
config: config,
sdkClient: client,
}, nil
}
func (d *AliyunOSSDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
if d.config.Bucket == "" {
return nil, errors.New("config `bucket` is required")
}
if d.config.Domain == "" {
return nil, errors.New("config `domain` is required")
}
// 为存储空间绑定自定义域名
// REF: https://help.aliyun.com/zh/oss/developer-reference/putcname
err := d.sdkClient.PutBucketCnameWithCertificate(d.config.Bucket, oss.PutBucketCname{
Cname: d.config.Domain,
CertificateConfiguration: &oss.CertificateConfiguration{
Certificate: certPem,
PrivateKey: privkeyPem,
Force: true,
},
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'oss.PutBucketCnameWithCertificate'")
}
return &deployer.DeployResult{}, nil
}
func createSdkClient(accessKeyId, accessKeySecret, region string) (*oss.Client, error) {
// 接入点一览 https://help.aliyun.com/zh/oss/user-guide/regions-and-endpoints
var endpoint string
switch region {
case "":
endpoint = "oss.aliyuncs.com"
case
"cn-hzjbp",
"cn-hzjbp-a",
"cn-hzjbp-b":
endpoint = "oss-cn-hzjbp-a-internal.aliyuncs.com"
case
"cn-shanghai-finance-1",
"cn-shenzhen-finance-1",
"cn-beijing-finance-1",
"cn-north-2-gov-1":
endpoint = fmt.Sprintf("oss-%s-internal.aliyuncs.com", region)
default:
endpoint = fmt.Sprintf("oss-%s.aliyuncs.com", region)
}
client, err := oss.New(endpoint, accessKeyId, accessKeySecret)
if err != nil {
return nil, err
}
return client, nil
}

View File

@ -0,0 +1,85 @@
package aliyunoss_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-oss"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fAccessKeySecret string
fRegion string
fBucket string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_ALIYUNOSS_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "")
flag.StringVar(&fRegion, argsPrefix+"REGION", "", "")
flag.StringVar(&fBucket, argsPrefix+"BUCKET", "", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v aliyun_oss_test.go -args \
--CERTIMATE_DEPLOYER_ALIYUNOSS_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_ALIYUNOSS_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_ALIYUNOSS_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_DEPLOYER_ALIYUNOSS_ACCESSKEYSECRET="your-access-key-secret" \
--CERTIMATE_DEPLOYER_ALIYUNOSS_REGION="cn-hangzhou" \
--CERTIMATE_DEPLOYER_ALIYUNOSS_BUCKET="your-oss-bucket" \
--CERTIMATE_DEPLOYER_ALIYUNOSS_DOMAIN="example.com"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret),
fmt.Sprintf("REGION: %v", fRegion),
fmt.Sprintf("BUCKET: %v", fBucket),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.New(&provider.AliyunOSSDeployerConfig{
AccessKeyId: fAccessKeyId,
AccessKeySecret: fAccessKeySecret,
Region: fRegion,
Bucket: fBucket,
Domain: fDomain,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@ -0,0 +1,86 @@
package baiducloudcdn
import (
"context"
"errors"
"fmt"
"time"
bceCdn "github.com/baidubce/bce-sdk-go/services/cdn"
bceCdnApi "github.com/baidubce/bce-sdk-go/services/cdn/api"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
)
type BaiduCloudCDNDeployerConfig struct {
// 百度智能云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 百度智能云 SecretAccessKey。
SecretAccessKey string `json:"secretAccessKey"`
// 加速域名(不支持泛域名)。
Domain string `json:"domain"`
}
type BaiduCloudCDNDeployer struct {
config *BaiduCloudCDNDeployerConfig
logger deployer.Logger
sdkClient *bceCdn.Client
}
var _ deployer.Deployer = (*BaiduCloudCDNDeployer)(nil)
func New(config *BaiduCloudCDNDeployerConfig) (*BaiduCloudCDNDeployer, error) {
return NewWithLogger(config, deployer.NewNilLogger())
}
func NewWithLogger(config *BaiduCloudCDNDeployerConfig, logger deployer.Logger) (*BaiduCloudCDNDeployer, error) {
if config == nil {
return nil, errors.New("config is nil")
}
if logger == nil {
return nil, errors.New("logger is nil")
}
client, err := createSdkClient(config.AccessKeyId, config.SecretAccessKey)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
return &BaiduCloudCDNDeployer{
logger: logger,
config: config,
sdkClient: client,
}, nil
}
func (d *BaiduCloudCDNDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
// 修改域名证书
// REF: https://cloud.baidu.com/doc/CDN/s/qjzuz2hp8
putCertResp, err := d.sdkClient.PutCert(
d.config.Domain,
&bceCdnApi.UserCertificate{
CertName: fmt.Sprintf("certimate-%d", time.Now().UnixMilli()),
ServerData: certPem,
PrivateData: privkeyPem,
},
"ON",
)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.PutCert'")
}
d.logger.Logt("已修改域名证书", putCertResp)
return &deployer.DeployResult{}, nil
}
func createSdkClient(accessKeyId, secretAccessKey string) (*bceCdn.Client, error) {
client, err := bceCdn.NewClient(accessKeyId, secretAccessKey, "")
if err != nil {
return nil, err
}
return client, nil
}

View File

@ -0,0 +1,75 @@
package baiducloudcdn_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baiducloud-cdn"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fSecretAccessKey string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_BAIDUCLOUDCDN_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v baiducloud_cdn_test.go -args \
--CERTIMATE_DEPLOYER_BAIDUCLOUDCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_BAIDUCLOUDCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_BAIDUCLOUDCDN_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_DEPLOYER_BAIDUCLOUDCDN_SECRETACCESSKEY="your-secret-access-key" \
--CERTIMATE_DEPLOYER_BAIDUCLOUDCDN_DOMAIN="example.com"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.New(&provider.BaiduCloudCDNDeployerConfig{
AccessKeyId: fAccessKeyId,
SecretAccessKey: fSecretAccessKey,
Domain: fDomain,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@ -0,0 +1,136 @@
package bytepluscdn
import (
"context"
"errors"
"fmt"
"strings"
bpCdn "github.com/byteplus-sdk/byteplus-sdk-golang/service/cdn"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
providerCdn "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/byteplus-cdn"
)
type BytePlusCDNDeployerConfig struct {
// BytePlus AccessKey。
AccessKey string `json:"accessKey"`
// BytePlus SecretKey。
SecretKey string `json:"secretKey"`
// 加速域名(支持泛域名)。
Domain string `json:"domain"`
}
type BytePlusCDNDeployer struct {
config *BytePlusCDNDeployerConfig
logger deployer.Logger
sdkClient *bpCdn.CDN
sslUploader uploader.Uploader
}
var _ deployer.Deployer = (*BytePlusCDNDeployer)(nil)
func New(config *BytePlusCDNDeployerConfig) (*BytePlusCDNDeployer, error) {
return NewWithLogger(config, deployer.NewNilLogger())
}
func NewWithLogger(config *BytePlusCDNDeployerConfig, logger deployer.Logger) (*BytePlusCDNDeployer, error) {
if config == nil {
return nil, errors.New("config is nil")
}
if logger == nil {
return nil, errors.New("logger is nil")
}
client := bpCdn.NewInstance()
client.Client.SetAccessKey(config.AccessKey)
client.Client.SetSecretKey(config.SecretKey)
uploader, err := providerCdn.New(&providerCdn.ByteplusCDNUploaderConfig{
AccessKey: config.AccessKey,
SecretKey: config.SecretKey,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &BytePlusCDNDeployer{
logger: logger,
config: config,
sdkClient: client,
sslUploader: uploader,
}, nil
}
func (d *BytePlusCDNDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
// 上传证书到 CDN
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
if err != nil {
return nil, xerrors.Wrap(err, "failed to upload certificate file")
}
d.logger.Logt("certificate file uploaded", upres)
domains := make([]string, 0)
if strings.HasPrefix(d.config.Domain, "*.") {
// 获取指定证书可关联的域名
// REF: https://docs.byteplus.com/en/docs/byteplus-cdn/reference-describecertconfig-9ea17
describeCertConfigReq := &bpCdn.DescribeCertConfigRequest{
CertId: upres.CertId,
}
describeCertConfigResp, err := d.sdkClient.DescribeCertConfig(describeCertConfigReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.DescribeCertConfig'")
}
if describeCertConfigResp.Result.CertNotConfig != nil {
for i := range describeCertConfigResp.Result.CertNotConfig {
domains = append(domains, describeCertConfigResp.Result.CertNotConfig[i].Domain)
}
}
if describeCertConfigResp.Result.OtherCertConfig != nil {
for i := range describeCertConfigResp.Result.OtherCertConfig {
domains = append(domains, describeCertConfigResp.Result.OtherCertConfig[i].Domain)
}
}
if len(domains) == 0 {
if len(describeCertConfigResp.Result.SpecifiedCertConfig) > 0 {
// 所有可关联的域名都配置了该证书,跳过部署
} else {
return nil, xerrors.New("domain not found")
}
}
} else {
domains = append(domains, d.config.Domain)
}
if len(domains) > 0 {
var errs []error
for _, domain := range domains {
// 关联证书与加速域名
// REF: https://docs.byteplus.com/en/docs/byteplus-cdn/reference-batchdeploycert
batchDeployCertReq := &bpCdn.BatchDeployCertRequest{
CertId: upres.CertId,
Domain: domain,
}
batchDeployCertResp, err := d.sdkClient.BatchDeployCert(batchDeployCertReq)
if err != nil {
errs = append(errs, err)
} else {
d.logger.Logt(fmt.Sprintf("已关联证书到域名 %s", domain), batchDeployCertResp)
}
}
if len(errs) > 0 {
return nil, errors.Join(errs...)
}
}
return &deployer.DeployResult{}, nil
}

View File

@ -0,0 +1,75 @@
package bytepluscdn_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/byteplus-cdn"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKey string
fSecretKey string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_BYTEPLUSCDN_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKey, argsPrefix+"ACCESSKEY", "", "")
flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v byteplus_cdn_test.go -args \
--CERTIMATE_DEPLOYER_BYTEPLUSCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_BYTEPLUSCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_BYTEPLUSCDN_ACCESSKEY="your-access-key" \
--CERTIMATE_DEPLOYER_BYTEPLUSCDN_SECRETKEY="your-secret-key" \
--CERTIMATE_DEPLOYER_BYTEPLUSCDN_DOMAIN="example.com"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("ACCESSKEY: %v", fAccessKey),
fmt.Sprintf("SECRETKEY: %v", fSecretKey),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.New(&provider.BytePlusCDNDeployerConfig{
AccessKey: fAccessKey,
SecretKey: fSecretKey,
Domain: fDomain,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@ -0,0 +1,85 @@
package dogecloudcdn
import (
"context"
"errors"
"strconv"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
providerDoge "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/dogecloud"
dogesdk "github.com/usual2970/certimate/internal/pkg/vendors/dogecloud-sdk"
)
type DogeCloudCDNDeployerConfig struct {
// 多吉云 AccessKey。
AccessKey string `json:"accessKey"`
// 多吉云 SecretKey。
SecretKey string `json:"secretKey"`
// 加速域名(不支持泛域名)。
Domain string `json:"domain"`
}
type DogeCloudCDNDeployer struct {
config *DogeCloudCDNDeployerConfig
logger deployer.Logger
sdkClient *dogesdk.Client
sslUploader uploader.Uploader
}
var _ deployer.Deployer = (*DogeCloudCDNDeployer)(nil)
func New(config *DogeCloudCDNDeployerConfig) (*DogeCloudCDNDeployer, error) {
return NewWithLogger(config, deployer.NewNilLogger())
}
func NewWithLogger(config *DogeCloudCDNDeployerConfig, logger deployer.Logger) (*DogeCloudCDNDeployer, error) {
if config == nil {
return nil, errors.New("config is nil")
}
if logger == nil {
return nil, errors.New("logger is nil")
}
client := dogesdk.NewClient(config.AccessKey, config.SecretKey)
uploader, err := providerDoge.New(&providerDoge.DogeCloudUploaderConfig{
AccessKey: config.AccessKey,
SecretKey: config.SecretKey,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &DogeCloudCDNDeployer{
logger: logger,
config: config,
sdkClient: client,
sslUploader: uploader,
}, nil
}
func (d *DogeCloudCDNDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
// 上传证书到 CDN
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
if err != nil {
return nil, xerrors.Wrap(err, "failed to upload certificate file")
}
d.logger.Logt("certificate file uploaded", upres)
// 绑定证书
// REF: https://docs.dogecloud.com/cdn/api-cert-bind
bindCdnCertId, _ := strconv.ParseInt(upres.CertId, 10, 64)
bindCdnCertResp, err := d.sdkClient.BindCdnCertWithDomain(bindCdnCertId, d.config.Domain)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.BindCdnCert'")
}
d.logger.Logt("已绑定证书", bindCdnCertResp)
return &deployer.DeployResult{}, nil
}

View File

@ -0,0 +1,75 @@
package dogecloudcdn_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/dogecloud-cdn"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKey string
fSecretKey string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_DOGECLOUDCDN_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKey, argsPrefix+"ACCESSKEY", "", "")
flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v dogecloud_cdn_test.go -args \
--CERTIMATE_DEPLOYER_DOGECLOUDCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_DOGECLOUDCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_DOGECLOUDCDN_ACCESSKEY="your-access-key" \
--CERTIMATE_DEPLOYER_DOGECLOUDCDN_SECRETKEY="your-secret-key" \
--CERTIMATE_DEPLOYER_DOGECLOUDCDN_DOMAIN="example.com"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("ACCESSKEY: %v", fAccessKey),
fmt.Sprintf("SECRETKEY: %v", fSecretKey),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.New(&provider.DogeCloudCDNDeployerConfig{
AccessKey: fAccessKey,
SecretKey: fSecretKey,
Domain: fDomain,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@ -0,0 +1,152 @@
package huaweicloudcdn
import (
"context"
"errors"
"github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/global"
hcCdn "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/cdn/v2"
hcCdnModel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/cdn/v2/model"
hcCdnRegion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/cdn/v2/region"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
providerScm "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/huaweicloud-scm"
"github.com/usual2970/certimate/internal/pkg/utils/cast"
huaweicloudsdk "github.com/usual2970/certimate/internal/pkg/vendors/huaweicloud-cdn-sdk"
)
type HuaweiCloudCDNDeployerConfig struct {
// 华为云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 华为云 SecretAccessKey。
SecretAccessKey string `json:"secretAccessKey"`
// 华为云地域。
Region string `json:"region"`
// 加速域名(不支持泛域名)。
Domain string `json:"domain"`
}
type HuaweiCloudCDNDeployer struct {
config *HuaweiCloudCDNDeployerConfig
logger deployer.Logger
sdkClient *huaweicloudsdk.Client
sslUploader uploader.Uploader
}
var _ deployer.Deployer = (*HuaweiCloudCDNDeployer)(nil)
func New(config *HuaweiCloudCDNDeployerConfig) (*HuaweiCloudCDNDeployer, error) {
return NewWithLogger(config, deployer.NewNilLogger())
}
func NewWithLogger(config *HuaweiCloudCDNDeployerConfig, logger deployer.Logger) (*HuaweiCloudCDNDeployer, error) {
if config == nil {
return nil, errors.New("config is nil")
}
if logger == nil {
return nil, errors.New("logger is nil")
}
client, err := createSdkClient(
config.AccessKeyId,
config.SecretAccessKey,
config.Region,
)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
uploader, err := providerScm.New(&providerScm.HuaweiCloudSCMUploaderConfig{
AccessKeyId: config.AccessKeyId,
SecretAccessKey: config.SecretAccessKey,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &HuaweiCloudCDNDeployer{
logger: logger,
config: config,
sdkClient: client,
sslUploader: uploader,
}, nil
}
func (d *HuaweiCloudCDNDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
// 上传证书到 SCM
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
if err != nil {
return nil, xerrors.Wrap(err, "failed to upload certificate file")
}
d.logger.Logt("certificate file uploaded", upres)
// 查询加速域名配置
// REF: https://support.huaweicloud.com/api-cdn/ShowDomainFullConfig.html
showDomainFullConfigReq := &hcCdnModel.ShowDomainFullConfigRequest{
DomainName: d.config.Domain,
}
showDomainFullConfigResp, err := d.sdkClient.ShowDomainFullConfig(showDomainFullConfigReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.ShowDomainFullConfig'")
}
d.logger.Logt("已查询到加速域名配置", showDomainFullConfigResp)
// 更新加速域名配置
// REF: https://support.huaweicloud.com/api-cdn/UpdateDomainMultiCertificates.html
// REF: https://support.huaweicloud.com/usermanual-cdn/cdn_01_0306.html
updateDomainMultiCertificatesReqBodyContent := &huaweicloudsdk.UpdateDomainMultiCertificatesExRequestBodyContent{}
updateDomainMultiCertificatesReqBodyContent.DomainName = d.config.Domain
updateDomainMultiCertificatesReqBodyContent.HttpsSwitch = 1
updateDomainMultiCertificatesReqBodyContent.CertificateType = cast.Int32Ptr(2)
updateDomainMultiCertificatesReqBodyContent.SCMCertificateId = cast.StringPtr(upres.CertId)
updateDomainMultiCertificatesReqBodyContent.CertName = cast.StringPtr(upres.CertName)
updateDomainMultiCertificatesReqBodyContent = updateDomainMultiCertificatesReqBodyContent.MergeConfig(showDomainFullConfigResp.Configs)
updateDomainMultiCertificatesReq := &huaweicloudsdk.UpdateDomainMultiCertificatesExRequest{
Body: &huaweicloudsdk.UpdateDomainMultiCertificatesExRequestBody{
Https: updateDomainMultiCertificatesReqBodyContent,
},
}
updateDomainMultiCertificatesResp, err := d.sdkClient.UploadDomainMultiCertificatesEx(updateDomainMultiCertificatesReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.UploadDomainMultiCertificatesEx'")
}
d.logger.Logt("已更新加速域名配置", updateDomainMultiCertificatesResp)
return &deployer.DeployResult{}, nil
}
func createSdkClient(accessKeyId, secretAccessKey, region string) (*huaweicloudsdk.Client, error) {
if region == "" {
region = "cn-north-1" // CDN 服务默认区域:华北一北京
}
auth, err := global.NewCredentialsBuilder().
WithAk(accessKeyId).
WithSk(secretAccessKey).
SafeBuild()
if err != nil {
return nil, err
}
hcRegion, err := hcCdnRegion.SafeValueOf(region)
if err != nil {
return nil, err
}
hcClient, err := hcCdn.CdnClientBuilder().
WithRegion(hcRegion).
WithCredential(auth).
SafeBuild()
if err != nil {
return nil, err
}
client := huaweicloudsdk.NewClient(hcClient)
return client, nil
}

View File

@ -0,0 +1,80 @@
package huaweicloudcdn_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/huaweicloud-cdn"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fSecretAccessKey string
fRegion string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_HUAWEICLOUDCDN_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "")
flag.StringVar(&fRegion, argsPrefix+"REGION", "", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v huaweicloud_cdn_test.go -args \
--CERTIMATE_DEPLOYER_HUAWEICLOUDCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_HUAWEICLOUDCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_HUAWEICLOUDCDN_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_DEPLOYER_HUAWEICLOUDCDN_SECRETACCESSKEY="your-secret-access-key" \
--CERTIMATE_DEPLOYER_HUAWEICLOUDCDN_REGION="cn-north-1" \
--CERTIMATE_DEPLOYER_HUAWEICLOUDCDN_DOMAIN="example.com"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey),
fmt.Sprintf("REGION: %v", fRegion),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.New(&provider.HuaweiCloudCDNDeployerConfig{
AccessKeyId: fAccessKeyId,
SecretAccessKey: fSecretAccessKey,
Region: fRegion,
Domain: fDomain,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@ -0,0 +1,12 @@
package huaweicloudelb
type DeployResourceType string
const (
// 资源类型:替换指定证书。
DEPLOY_RESOURCE_CERTIFICATE = DeployResourceType("certificate")
// 资源类型:部署到指定负载均衡器。
DEPLOY_RESOURCE_LOADBALANCER = DeployResourceType("loadbalancer")
// 资源类型:部署到指定监听器。
DEPLOY_RESOURCE_LISTENER = DeployResourceType("listener")
)

View File

@ -0,0 +1,395 @@
package huaweicloudelb
import (
"context"
"errors"
"fmt"
"github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/basic"
"github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/global"
hcElb "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3"
hcElbModel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3/model"
hcElbRegion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3/region"
hcIam "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3"
hcIamModel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3/model"
hcIamRegion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3/region"
xerrors "github.com/pkg/errors"
"golang.org/x/exp/slices"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
providerElb "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/huaweicloud-elb"
"github.com/usual2970/certimate/internal/pkg/utils/cast"
)
type HuaweiCloudELBDeployerConfig struct {
// 华为云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 华为云 SecretAccessKey。
SecretAccessKey string `json:"secretAccessKey"`
// 华为云地域。
Region string `json:"region"`
// 部署资源类型。
ResourceType DeployResourceType `json:"resourceType"`
// 证书 ID。
// 部署资源类型为 [DEPLOY_RESOURCE_CERTIFICATE] 时必填。
CertificateId string `json:"certificateId,omitempty"`
// 负载均衡器 ID。
// 部署资源类型为 [DEPLOY_RESOURCE_LOADBALANCER] 时必填。
LoadbalancerId string `json:"loadbalancerId,omitempty"`
// 负载均衡监听 ID。
// 部署资源类型为 [DEPLOY_RESOURCE_LISTENER] 时必填。
ListenerId string `json:"listenerId,omitempty"`
}
type HuaweiCloudELBDeployer struct {
config *HuaweiCloudELBDeployerConfig
logger deployer.Logger
sdkClient *hcElb.ElbClient
sslUploader uploader.Uploader
}
var _ deployer.Deployer = (*HuaweiCloudELBDeployer)(nil)
func New(config *HuaweiCloudELBDeployerConfig) (*HuaweiCloudELBDeployer, error) {
return NewWithLogger(config, deployer.NewNilLogger())
}
func NewWithLogger(config *HuaweiCloudELBDeployerConfig, logger deployer.Logger) (*HuaweiCloudELBDeployer, error) {
if config == nil {
return nil, errors.New("config is nil")
}
if logger == nil {
return nil, errors.New("logger is nil")
}
client, err := createSdkClient(config.AccessKeyId, config.SecretAccessKey, config.Region)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
uploader, err := providerElb.New(&providerElb.HuaweiCloudELBUploaderConfig{
AccessKeyId: config.AccessKeyId,
SecretAccessKey: config.SecretAccessKey,
Region: config.Region,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &HuaweiCloudELBDeployer{
logger: logger,
config: config,
sdkClient: client,
sslUploader: uploader,
}, nil
}
func (d *HuaweiCloudELBDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
// 上传证书到 SCM
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
if err != nil {
return nil, xerrors.Wrap(err, "failed to upload certificate file")
}
d.logger.Logt("certificate file uploaded", upres)
// 根据部署资源类型决定部署方式
switch d.config.ResourceType {
case DEPLOY_RESOURCE_CERTIFICATE:
if err := d.deployToCertificate(ctx, certPem, privkeyPem); err != nil {
return nil, err
}
case DEPLOY_RESOURCE_LOADBALANCER:
if err := d.deployToLoadbalancer(ctx, certPem, privkeyPem); err != nil {
return nil, err
}
case DEPLOY_RESOURCE_LISTENER:
if err := d.deployToListener(ctx, certPem, privkeyPem); err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unsupported resource type: %s", d.config.ResourceType)
}
return &deployer.DeployResult{}, nil
}
func (d *HuaweiCloudELBDeployer) deployToCertificate(ctx context.Context, certPem string, privkeyPem string) error {
if d.config.CertificateId == "" {
return errors.New("config `certificateId` is required")
}
// 更新证书
// REF: https://support.huaweicloud.com/api-elb/UpdateCertificate.html
updateCertificateReq := &hcElbModel.UpdateCertificateRequest{
CertificateId: d.config.CertificateId,
Body: &hcElbModel.UpdateCertificateRequestBody{
Certificate: &hcElbModel.UpdateCertificateOption{
Certificate: cast.StringPtr(certPem),
PrivateKey: cast.StringPtr(privkeyPem),
},
},
}
updateCertificateResp, err := d.sdkClient.UpdateCertificate(updateCertificateReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'elb.UpdateCertificate'")
}
d.logger.Logt("已更新 ELB 证书", updateCertificateResp)
return nil
}
func (d *HuaweiCloudELBDeployer) deployToLoadbalancer(ctx context.Context, certPem string, privkeyPem string) error {
if d.config.LoadbalancerId == "" {
return errors.New("config `loadbalancerId` is required")
}
listenerIds := make([]string, 0)
// 查询负载均衡器详情
// REF: https://support.huaweicloud.com/api-elb/ShowLoadBalancer.html
showLoadBalancerReq := &hcElbModel.ShowLoadBalancerRequest{
LoadbalancerId: d.config.LoadbalancerId,
}
showLoadBalancerResp, err := d.sdkClient.ShowLoadBalancer(showLoadBalancerReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'elb.ShowLoadBalancer'")
}
d.logger.Logt("已查询到 ELB 负载均衡器", showLoadBalancerResp)
// 查询监听器列表
// REF: https://support.huaweicloud.com/api-elb/ListListeners.html
listListenersLimit := int32(2000)
var listListenersMarker *string = nil
for {
listListenersReq := &hcElbModel.ListListenersRequest{
Limit: cast.Int32Ptr(listListenersLimit),
Marker: listListenersMarker,
Protocol: &[]string{"HTTPS", "TERMINATED_HTTPS"},
LoadbalancerId: &[]string{showLoadBalancerResp.Loadbalancer.Id},
}
listListenersResp, err := d.sdkClient.ListListeners(listListenersReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'elb.ListListeners'")
}
if listListenersResp.Listeners != nil {
for _, listener := range *listListenersResp.Listeners {
listenerIds = append(listenerIds, listener.Id)
}
}
if listListenersResp.Listeners == nil || len(*listListenersResp.Listeners) < int(listListenersLimit) {
break
} else {
listListenersMarker = listListenersResp.PageInfo.NextMarker
}
}
d.logger.Logt("已查询到 ELB 负载均衡器下的监听器", listenerIds)
// 上传证书到 SCM
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
if err != nil {
return xerrors.Wrap(err, "failed to upload certificate file")
}
d.logger.Logt("certificate file uploaded", upres)
// 批量更新监听器证书
var errs []error
for _, listenerId := range listenerIds {
if err := d.modifyListenerCertificate(ctx, listenerId, upres.CertId); err != nil {
errs = append(errs, err)
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
func (d *HuaweiCloudELBDeployer) deployToListener(ctx context.Context, certPem string, privkeyPem string) error {
if d.config.ListenerId == "" {
return errors.New("config `listenerId` is required")
}
// 上传证书到 SCM
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
if err != nil {
return xerrors.Wrap(err, "failed to upload certificate file")
}
d.logger.Logt("certificate file uploaded", upres)
// 更新监听器证书
if err := d.modifyListenerCertificate(ctx, d.config.ListenerId, upres.CertId); err != nil {
return err
}
return nil
}
func (d *HuaweiCloudELBDeployer) modifyListenerCertificate(ctx context.Context, cloudListenerId string, cloudCertId string) error {
// 查询监听器详情
// REF: https://support.huaweicloud.com/api-elb/ShowListener.html
showListenerReq := &hcElbModel.ShowListenerRequest{
ListenerId: cloudListenerId,
}
showListenerResp, err := d.sdkClient.ShowListener(showListenerReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'elb.ShowListener'")
}
d.logger.Logt("已查询到 ELB 监听器", showListenerResp)
// 更新监听器
// REF: https://support.huaweicloud.com/api-elb/UpdateListener.html
updateListenerReq := &hcElbModel.UpdateListenerRequest{
ListenerId: cloudListenerId,
Body: &hcElbModel.UpdateListenerRequestBody{
Listener: &hcElbModel.UpdateListenerOption{
DefaultTlsContainerRef: cast.StringPtr(cloudCertId),
},
},
}
if showListenerResp.Listener.SniContainerRefs != nil {
if len(showListenerResp.Listener.SniContainerRefs) > 0 {
// 如果开启 SNI需替换同 SAN 的证书
sniCertIds := make([]string, 0)
sniCertIds = append(sniCertIds, cloudCertId)
listOldCertificateReq := &hcElbModel.ListCertificatesRequest{
Id: &showListenerResp.Listener.SniContainerRefs,
}
listOldCertificateResp, err := d.sdkClient.ListCertificates(listOldCertificateReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'elb.ListCertificates'")
}
showNewCertificateReq := &hcElbModel.ShowCertificateRequest{
CertificateId: cloudCertId,
}
showNewCertificateResp, err := d.sdkClient.ShowCertificate(showNewCertificateReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'elb.ShowCertificate'")
}
for _, certificate := range *listOldCertificateResp.Certificates {
oldCertificate := certificate
newCertificate := showNewCertificateResp.Certificate
if oldCertificate.SubjectAlternativeNames != nil && newCertificate.SubjectAlternativeNames != nil {
if slices.Equal(*oldCertificate.SubjectAlternativeNames, *newCertificate.SubjectAlternativeNames) {
continue
}
} else {
if oldCertificate.Domain == newCertificate.Domain {
continue
}
}
sniCertIds = append(sniCertIds, certificate.Id)
}
updateListenerReq.Body.Listener.SniContainerRefs = &sniCertIds
}
if showListenerResp.Listener.SniMatchAlgo != "" {
updateListenerReq.Body.Listener.SniMatchAlgo = cast.StringPtr(showListenerResp.Listener.SniMatchAlgo)
}
}
updateListenerResp, err := d.sdkClient.UpdateListener(updateListenerReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'elb.UpdateListener'")
}
d.logger.Logt("已更新 ELB 监听器", updateListenerResp)
return nil
}
func createSdkClient(accessKeyId, secretAccessKey, region string) (*hcElb.ElbClient, error) {
if region == "" {
region = "cn-north-4" // ELB 服务默认区域:华北四北京
}
projectId, err := getSdkProjectId(accessKeyId, secretAccessKey, region)
if err != nil {
return nil, err
}
auth, err := basic.NewCredentialsBuilder().
WithAk(accessKeyId).
WithSk(secretAccessKey).
WithProjectId(projectId).
SafeBuild()
if err != nil {
return nil, err
}
hcRegion, err := hcElbRegion.SafeValueOf(region)
if err != nil {
return nil, err
}
hcClient, err := hcElb.ElbClientBuilder().
WithRegion(hcRegion).
WithCredential(auth).
SafeBuild()
if err != nil {
return nil, err
}
client := hcElb.NewElbClient(hcClient)
return client, nil
}
func getSdkProjectId(accessKeyId, secretAccessKey, region string) (string, error) {
if region == "" {
region = "cn-north-4" // IAM 服务默认区域:华北四北京
}
auth, err := global.NewCredentialsBuilder().
WithAk(accessKeyId).
WithSk(secretAccessKey).
SafeBuild()
if err != nil {
return "", err
}
hcRegion, err := hcIamRegion.SafeValueOf(region)
if err != nil {
return "", err
}
hcClient, err := hcIam.IamClientBuilder().
WithRegion(hcRegion).
WithCredential(auth).
SafeBuild()
if err != nil {
return "", err
}
client := hcIam.NewIamClient(hcClient)
request := &hcIamModel.KeystoneListProjectsRequest{
Name: &region,
}
response, err := client.KeystoneListProjects(request)
if err != nil {
return "", err
} else if response.Projects == nil || len(*response.Projects) == 0 {
return "", errors.New("no project found")
}
return (*response.Projects)[0].Id, nil
}

View File

@ -0,0 +1,155 @@
package huaweicloudelb_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/huaweicloud-elb"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fSecretAccessKey string
fRegion string
fCertificateId string
fLoadbalancerId string
fListenerId string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_HUAWEICLOUDELB_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "")
flag.StringVar(&fRegion, argsPrefix+"REGION", "", "")
flag.StringVar(&fCertificateId, argsPrefix+"CERTIFICATEID", "", "")
flag.StringVar(&fLoadbalancerId, argsPrefix+"LOADBALANCERID", "", "")
flag.StringVar(&fListenerId, argsPrefix+"LISTENERID", "", "")
}
/*
Shell command to run this test:
go test -v huaweicloud_elb_test.go -args \
--CERTIMATE_DEPLOYER_HUAWEICLOUDELB_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_HUAWEICLOUDELB_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_HUAWEICLOUDELB_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_DEPLOYER_HUAWEICLOUDELB_SECRETACCESSKEY="your-secret-access-key" \
--CERTIMATE_DEPLOYER_HUAWEICLOUDELB_REGION="cn-north-1" \
--CERTIMATE_DEPLOYER_HUAWEICLOUDELB_CERTIFICATEID="your-elb-cert-id" \
--CERTIMATE_DEPLOYER_HUAWEICLOUDELB_LOADBALANCERID="your-elb-loadbalancer-id" \
--CERTIMATE_DEPLOYER_HUAWEICLOUDELB_LISTENERID="your-elb-listener-id"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy_ToCertificate", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey),
fmt.Sprintf("REGION: %v", fRegion),
fmt.Sprintf("CERTIFICATEID: %v", fCertificateId),
}, "\n"))
deployer, err := provider.New(&provider.HuaweiCloudELBDeployerConfig{
AccessKeyId: fAccessKeyId,
SecretAccessKey: fSecretAccessKey,
Region: fRegion,
ResourceType: provider.DEPLOY_RESOURCE_CERTIFICATE,
CertificateId: fCertificateId,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
t.Run("Deploy_ToLoadbalancer", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey),
fmt.Sprintf("REGION: %v", fRegion),
fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId),
}, "\n"))
deployer, err := provider.New(&provider.HuaweiCloudELBDeployerConfig{
AccessKeyId: fAccessKeyId,
SecretAccessKey: fSecretAccessKey,
Region: fRegion,
ResourceType: provider.DEPLOY_RESOURCE_LOADBALANCER,
LoadbalancerId: fLoadbalancerId,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
t.Run("Deploy_ToListenerId", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey),
fmt.Sprintf("REGION: %v", fRegion),
fmt.Sprintf("LISTENERID: %v", fListenerId),
}, "\n"))
deployer, err := provider.New(&provider.HuaweiCloudELBDeployerConfig{
AccessKeyId: fAccessKeyId,
SecretAccessKey: fSecretAccessKey,
Region: fRegion,
ResourceType: provider.DEPLOY_RESOURCE_LISTENER,
ListenerId: fListenerId,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@ -0,0 +1,160 @@
package k8ssecret
import (
"context"
"errors"
"strings"
xerrors "github.com/pkg/errors"
k8sCore "k8s.io/api/core/v1"
k8sMeta "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/utils/x509"
)
type K8sSecretDeployerConfig struct {
// kubeconfig 文件内容。
KubeConfig string `json:"kubeConfig,omitempty"`
// K8s 命名空间。
Namespace string `json:"namespace,omitempty"`
// K8s Secret 名称。
SecretName string `json:"secretName"`
// K8s Secret 中用于存放证书的 Key。
SecretDataKeyForCrt string `json:"secretDataKeyForCrt,omitempty"`
// K8s Secret 中用于存放私钥的 Key。
SecretDataKeyForKey string `json:"secretDataKeyForKey,omitempty"`
}
type K8sSecretDeployer struct {
config *K8sSecretDeployerConfig
logger deployer.Logger
}
var _ deployer.Deployer = (*K8sSecretDeployer)(nil)
func New(config *K8sSecretDeployerConfig) (*K8sSecretDeployer, error) {
return NewWithLogger(config, deployer.NewNilLogger())
}
func NewWithLogger(config *K8sSecretDeployerConfig, logger deployer.Logger) (*K8sSecretDeployer, error) {
if config == nil {
return nil, errors.New("config is nil")
}
if logger == nil {
return nil, errors.New("logger is nil")
}
return &K8sSecretDeployer{
logger: logger,
config: config,
}, nil
}
func (d *K8sSecretDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
if d.config.Namespace == "" {
return nil, errors.New("config `namespace` is required")
}
if d.config.SecretName == "" {
return nil, errors.New("config `secretName` is required")
}
if d.config.SecretDataKeyForCrt == "" {
return nil, errors.New("config `secretDataKeyForCrt` is required")
}
if d.config.SecretDataKeyForKey == "" {
return nil, errors.New("config `secretDataKeyForKey` is required")
}
certX509, err := x509.ParseCertificateFromPEM(certPem)
if err != nil {
return nil, err
}
// 连接
client, err := createK8sClient(d.config.KubeConfig)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create k8s client")
}
var secretPayload *k8sCore.Secret
secretAnnotations := map[string]string{
"certimate/common-name": certX509.Subject.CommonName,
"certimate/subject-sn": certX509.Subject.SerialNumber,
"certimate/subject-alt-names": strings.Join(certX509.DNSNames, ","),
"certimate/issuer-sn": certX509.Issuer.SerialNumber,
"certimate/issuer-org": strings.Join(certX509.Issuer.Organization, ","),
}
// 获取 Secret 实例,如果不存在则创建
secretPayload, err = client.CoreV1().Secrets(d.config.Namespace).Get(context.TODO(), d.config.SecretName, k8sMeta.GetOptions{})
if err != nil {
secretPayload = &k8sCore.Secret{
TypeMeta: k8sMeta.TypeMeta{
Kind: "Secret",
APIVersion: "v1",
},
ObjectMeta: k8sMeta.ObjectMeta{
Name: d.config.SecretName,
Annotations: secretAnnotations,
},
Type: k8sCore.SecretType("kubernetes.io/tls"),
}
secretPayload.Data = make(map[string][]byte)
secretPayload.Data[d.config.SecretDataKeyForCrt] = []byte(certPem)
secretPayload.Data[d.config.SecretDataKeyForKey] = []byte(privkeyPem)
_, err = client.CoreV1().Secrets(d.config.Namespace).Create(context.TODO(), secretPayload, k8sMeta.CreateOptions{})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create k8s secret")
} else {
d.logger.Logf("k8s secret created", secretPayload)
return &deployer.DeployResult{}, nil
}
}
// 更新 Secret 实例
secretPayload.Type = k8sCore.SecretType("kubernetes.io/tls")
if secretPayload.ObjectMeta.Annotations == nil {
secretPayload.ObjectMeta.Annotations = secretAnnotations
} else {
for k, v := range secretAnnotations {
secretPayload.ObjectMeta.Annotations[k] = v
}
}
secretPayload, err = client.CoreV1().Secrets(d.config.Namespace).Update(context.TODO(), secretPayload, k8sMeta.UpdateOptions{})
if err != nil {
return nil, xerrors.Wrap(err, "failed to update k8s secret")
}
d.logger.Logf("k8s secret updated", secretPayload)
return &deployer.DeployResult{}, nil
}
func createK8sClient(kubeConfig string) (*kubernetes.Clientset, error) {
var config *rest.Config
var err error
if kubeConfig == "" {
config, err = rest.InClusterConfig()
} else {
kubeConfig, err := clientcmd.NewClientConfigFromBytes([]byte(kubeConfig))
if err != nil {
return nil, err
}
config, err = kubeConfig.ClientConfig()
}
if err != nil {
return nil, err
}
client, err := kubernetes.NewForConfig(config)
if err != nil {
return nil, err
}
return client, nil
}

View File

@ -0,0 +1,80 @@
package k8ssecret_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/k8s-secret"
)
var (
fInputCertPath string
fInputKeyPath string
fNamespace string
fSecretName string
fSecretDataKeyForCrt string
fSecretDataKeyForKey string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_K8SSECRET_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fNamespace, argsPrefix+"NAMESPACE", "default", "")
flag.StringVar(&fSecretName, argsPrefix+"SECRETNAME", "", "")
flag.StringVar(&fSecretDataKeyForCrt, argsPrefix+"SECRETDATAKEYFORCRT", "tls.crt", "")
flag.StringVar(&fSecretDataKeyForKey, argsPrefix+"SECRETDATAKEYFORKEY", "tls.key", "")
}
/*
Shell command to run this test:
go test -v k8s_secret_test.go -args \
--CERTIMATE_DEPLOYER_K8SSECRET_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_K8SSECRET_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_K8SSECRET_NAMESPACE="default" \
--CERTIMATE_DEPLOYER_K8SSECRET_SECRETNAME="secret" \
--CERTIMATE_DEPLOYER_K8SSECRET_SECRETDATAKEYFORCRT="tls.crt" \
--CERTIMATE_DEPLOYER_K8SSECRET_SECRETDATAKEYFORKEY="tls.key"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("NAMESPACE: %v", fNamespace),
fmt.Sprintf("SECRETNAME: %v", fSecretName),
fmt.Sprintf("SECRETDATAKEYFORCRT: %v", fSecretDataKeyForCrt),
fmt.Sprintf("SECRETDATAKEYFORKEY: %v", fSecretDataKeyForKey),
}, "\n"))
deployer, err := provider.New(&provider.K8sSecretDeployerConfig{
Namespace: fNamespace,
SecretName: fSecretName,
SecretDataKeyForCrt: fSecretDataKeyForCrt,
SecretDataKeyForKey: fSecretDataKeyForKey,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@ -0,0 +1,17 @@
package local
type OutputFormatType string
const (
OUTPUT_FORMAT_PEM = OutputFormatType("PEM")
OUTPUT_FORMAT_PFX = OutputFormatType("PFX")
OUTPUT_FORMAT_JKS = OutputFormatType("JKS")
)
type ShellEnvType string
const (
SHELL_ENV_SH = ShellEnvType("sh")
SHELL_ENV_CMD = ShellEnvType("cmd")
SHELL_ENV_POWERSHELL = ShellEnvType("powershell")
)

View File

@ -0,0 +1,178 @@
package local
import (
"bytes"
"context"
"errors"
"fmt"
"os/exec"
"runtime"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/utils/fs"
"github.com/usual2970/certimate/internal/pkg/utils/x509"
)
type LocalDeployerConfig struct {
// Shell 执行环境。
// 零值时默认根据操作系统决定。
ShellEnv ShellEnvType `json:"shellEnv,omitempty"`
// 前置命令。
PreCommand string `json:"preCommand,omitempty"`
// 后置命令。
PostCommand string `json:"postCommand,omitempty"`
// 输出证书格式。
OutputFormat OutputFormatType `json:"outputFormat,omitempty"`
// 输出证书文件路径。
OutputCertPath string `json:"outputCertPath,omitempty"`
// 输出私钥文件路径。
OutputKeyPath string `json:"outputKeyPath,omitempty"`
// PFX 导出密码。
// 证书格式为 PFX 时必填。
PfxPassword string `json:"pfxPassword,omitempty"`
// JKS 别名。
// 证书格式为 JKS 时必填。
JksAlias string `json:"jksAlias,omitempty"`
// JKS 密钥密码。
// 证书格式为 JKS 时必填。
JksKeypass string `json:"jksKeypass,omitempty"`
// JKS 存储密码。
// 证书格式为 JKS 时必填。
JksStorepass string `json:"jksStorepass,omitempty"`
}
type LocalDeployer struct {
config *LocalDeployerConfig
logger deployer.Logger
}
var _ deployer.Deployer = (*LocalDeployer)(nil)
func New(config *LocalDeployerConfig) (*LocalDeployer, error) {
return NewWithLogger(config, deployer.NewNilLogger())
}
func NewWithLogger(config *LocalDeployerConfig, logger deployer.Logger) (*LocalDeployer, error) {
if config == nil {
return nil, errors.New("config is nil")
}
if logger == nil {
return nil, errors.New("logger is nil")
}
return &LocalDeployer{
logger: logger,
config: config,
}, nil
}
func (d *LocalDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
// 执行前置命令
if d.config.PreCommand != "" {
stdout, stderr, err := execCommand(d.config.ShellEnv, d.config.PreCommand)
if err != nil {
return nil, xerrors.Wrapf(err, "failed to run pre-command, stdout: %s, stderr: %s", stdout, stderr)
}
d.logger.Logt("pre-command executed", stdout)
}
// 写入证书和私钥文件
switch d.config.OutputFormat {
case OUTPUT_FORMAT_PEM:
if err := fs.WriteFileString(d.config.OutputCertPath, certPem); err != nil {
return nil, xerrors.Wrap(err, "failed to save certificate file")
}
d.logger.Logt("certificate file saved")
if err := fs.WriteFileString(d.config.OutputKeyPath, privkeyPem); err != nil {
return nil, xerrors.Wrap(err, "failed to save private key file")
}
d.logger.Logt("private key file saved")
case OUTPUT_FORMAT_PFX:
pfxData, err := x509.TransformCertificateFromPEMToPFX(certPem, privkeyPem, d.config.PfxPassword)
if err != nil {
return nil, xerrors.Wrap(err, "failed to transform certificate to PFX")
}
d.logger.Logt("certificate transformed to PFX")
if err := fs.WriteFile(d.config.OutputCertPath, pfxData); err != nil {
return nil, xerrors.Wrap(err, "failed to save certificate file")
}
d.logger.Logt("certificate file saved")
case OUTPUT_FORMAT_JKS:
jksData, err := x509.TransformCertificateFromPEMToJKS(certPem, privkeyPem, d.config.JksAlias, d.config.JksKeypass, d.config.JksStorepass)
if err != nil {
return nil, xerrors.Wrap(err, "failed to transform certificate to JKS")
}
d.logger.Logt("certificate transformed to JKS")
if err := fs.WriteFile(d.config.OutputCertPath, jksData); err != nil {
return nil, xerrors.Wrap(err, "failed to save certificate file")
}
d.logger.Logt("certificate file uploaded")
default:
return nil, fmt.Errorf("unsupported output format: %s", d.config.OutputFormat)
}
// 执行后置命令
if d.config.PostCommand != "" {
stdout, stderr, err := execCommand(d.config.ShellEnv, d.config.PostCommand)
if err != nil {
return nil, xerrors.Wrapf(err, "failed to run command, stdout: %s, stderr: %s", stdout, stderr)
}
d.logger.Logt("post-command executed", stdout)
}
return &deployer.DeployResult{}, nil
}
func execCommand(shellEnv ShellEnvType, command string) (string, string, error) {
var cmd *exec.Cmd
switch shellEnv {
case SHELL_ENV_SH:
cmd = exec.Command("sh", "-c", command)
case SHELL_ENV_CMD:
cmd = exec.Command("cmd", "/C", command)
case SHELL_ENV_POWERSHELL:
cmd = exec.Command("powershell", "-Command", command)
case "":
if runtime.GOOS == "windows" {
cmd = exec.Command("cmd", "/C", command)
} else {
cmd = exec.Command("sh", "-c", command)
}
default:
return "", "", fmt.Errorf("unsupported shell env: %s", shellEnv)
}
var stdoutBuf bytes.Buffer
cmd.Stdout = &stdoutBuf
var stderrBuf bytes.Buffer
cmd.Stderr = &stderrBuf
err := cmd.Run()
if err != nil {
return "", "", xerrors.Wrap(err, "failed to execute shell command")
}
return stdoutBuf.String(), stderrBuf.String(), nil
}

View File

@ -0,0 +1,186 @@
package local_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/local"
)
var (
fInputCertPath string
fInputKeyPath string
fOutputCertPath string
fOutputKeyPath string
fPfxPassword string
fJksAlias string
fJksKeypass string
fJksStorepass string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_LOCAL_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fOutputCertPath, argsPrefix+"OUTPUTCERTPATH", "", "")
flag.StringVar(&fOutputKeyPath, argsPrefix+"OUTPUTKEYPATH", "", "")
flag.StringVar(&fPfxPassword, argsPrefix+"PFXPASSWORD", "", "")
flag.StringVar(&fJksAlias, argsPrefix+"JKSALIAS", "", "")
flag.StringVar(&fJksKeypass, argsPrefix+"JKSKEYPASS", "", "")
flag.StringVar(&fJksStorepass, argsPrefix+"JKSSTOREPASS", "", "")
}
/*
Shell command to run this test:
go test -v local_test.go -args \
--CERTIMATE_DEPLOYER_LOCAL_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_LOCAL_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_LOCAL_OUTPUTCERTPATH="/path/to/your-output-cert" \
--CERTIMATE_DEPLOYER_LOCAL_OUTPUTKEYPATH="/path/to/your-output-key" \
--CERTIMATE_DEPLOYER_LOCAL_PFXPASSWORD="your-pfx-password" \
--CERTIMATE_DEPLOYER_LOCAL_JKSALIAS="your-jks-alias" \
--CERTIMATE_DEPLOYER_LOCAL_JKSKEYPASS="your-jks-keypass" \
--CERTIMATE_DEPLOYER_LOCAL_JKSSTOREPASS="your-jks-storepass"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy_PEM", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("OUTPUTCERTPATH: %v", fOutputCertPath),
fmt.Sprintf("OUTPUTKEYPATH: %v", fOutputKeyPath),
}, "\n"))
deployer, err := provider.New(&provider.LocalDeployerConfig{
OutputCertPath: fOutputCertPath,
OutputKeyPath: fOutputKeyPath,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
fstat1, err := os.Stat(fOutputCertPath)
if err != nil {
t.Errorf("err: %+v", err)
return
} else if fstat1.Size() == 0 {
t.Errorf("err: empty output certificate file")
return
}
fstat2, err := os.Stat(fOutputKeyPath)
if err != nil {
t.Errorf("err: %+v", err)
return
} else if fstat2.Size() == 0 {
t.Errorf("err: empty output private key file")
return
}
t.Logf("ok: %v", res)
})
t.Run("Deploy_PFX", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("OUTPUTCERTPATH: %v", fOutputCertPath),
fmt.Sprintf("OUTPUTKEYPATH: %v", fOutputKeyPath),
fmt.Sprintf("PFXPASSWORD: %v", fPfxPassword),
}, "\n"))
deployer, err := provider.New(&provider.LocalDeployerConfig{
OutputFormat: provider.OUTPUT_FORMAT_PFX,
OutputCertPath: fOutputCertPath,
OutputKeyPath: fOutputKeyPath,
PfxPassword: fPfxPassword,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
fstat, err := os.Stat(fOutputCertPath)
if err != nil {
t.Errorf("err: %+v", err)
return
} else if fstat.Size() == 0 {
t.Errorf("err: empty output certificate file")
return
}
t.Logf("ok: %v", res)
})
t.Run("Deploy_JKS", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("OUTPUTCERTPATH: %v", fOutputCertPath),
fmt.Sprintf("OUTPUTKEYPATH: %v", fOutputKeyPath),
fmt.Sprintf("JKSALIAS: %v", fJksAlias),
fmt.Sprintf("JKSKEYPASS: %v", fJksKeypass),
fmt.Sprintf("JKSSTOREPASS: %v", fJksStorepass),
}, "\n"))
deployer, err := provider.New(&provider.LocalDeployerConfig{
OutputFormat: provider.OUTPUT_FORMAT_JKS,
OutputCertPath: fOutputCertPath,
OutputKeyPath: fOutputKeyPath,
JksAlias: fJksAlias,
JksKeypass: fJksKeypass,
JksStorepass: fJksStorepass,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
fstat, err := os.Stat(fOutputCertPath)
if err != nil {
t.Errorf("err: %+v", err)
return
} else if fstat.Size() == 0 {
t.Errorf("err: empty output certificate file")
return
}
t.Logf("ok: %v", res)
})
}

View File

@ -0,0 +1,106 @@
package qiniucdn
import (
"context"
"errors"
"strings"
xerrors "github.com/pkg/errors"
"github.com/qiniu/go-sdk/v7/auth"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
providerQiniu "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/qiniu-sslcert"
qiniusdk "github.com/usual2970/certimate/internal/pkg/vendors/qiniu-sdk"
)
type QiniuCDNDeployerConfig struct {
// 七牛云 AccessKey。
AccessKey string `json:"accessKey"`
// 七牛云 SecretKey。
SecretKey string `json:"secretKey"`
// 加速域名(支持泛域名)。
Domain string `json:"domain"`
}
type QiniuCDNDeployer struct {
config *QiniuCDNDeployerConfig
logger deployer.Logger
sdkClient *qiniusdk.Client
sslUploader uploader.Uploader
}
var _ deployer.Deployer = (*QiniuCDNDeployer)(nil)
func New(config *QiniuCDNDeployerConfig) (*QiniuCDNDeployer, error) {
return NewWithLogger(config, deployer.NewNilLogger())
}
func NewWithLogger(config *QiniuCDNDeployerConfig, logger deployer.Logger) (*QiniuCDNDeployer, error) {
if config == nil {
return nil, errors.New("config is nil")
}
if logger == nil {
return nil, errors.New("logger is nil")
}
client := qiniusdk.NewClient(auth.New(config.AccessKey, config.SecretKey))
uploader, err := providerQiniu.New(&providerQiniu.QiniuSSLCertUploaderConfig{
AccessKey: config.AccessKey,
SecretKey: config.SecretKey,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &QiniuCDNDeployer{
logger: logger,
config: config,
sdkClient: client,
sslUploader: uploader,
}, nil
}
func (d *QiniuCDNDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
// 上传证书到 CDN
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
if err != nil {
return nil, xerrors.Wrap(err, "failed to upload certificate file")
}
d.logger.Logt("certificate file uploaded", upres)
// "*.example.com" → ".example.com",适配七牛云 CDN 要求的泛域名格式
domain := strings.TrimPrefix(d.config.Domain, "*")
// 获取域名信息
// REF: https://developer.qiniu.com/fusion/4246/the-domain-name
getDomainInfoResp, err := d.sdkClient.GetDomainInfo(domain)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.GetDomainInfo'")
}
d.logger.Logt("已获取域名信息", getDomainInfoResp)
// 判断域名是否已启用 HTTPS。如果已启用修改域名证书否则启用 HTTPS
// REF: https://developer.qiniu.com/fusion/4246/the-domain-name
if getDomainInfoResp.Https != nil && getDomainInfoResp.Https.CertID != "" {
modifyDomainHttpsConfResp, err := d.sdkClient.ModifyDomainHttpsConf(domain, upres.CertId, getDomainInfoResp.Https.ForceHttps, getDomainInfoResp.Https.Http2Enable)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.ModifyDomainHttpsConf'")
}
d.logger.Logt("已修改域名证书", modifyDomainHttpsConfResp)
} else {
enableDomainHttpsResp, err := d.sdkClient.EnableDomainHttps(domain, upres.CertId, true, true)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.EnableDomainHttps'")
}
d.logger.Logt("已将域名升级为 HTTPS", enableDomainHttpsResp)
}
return &deployer.DeployResult{}, nil
}

View File

@ -0,0 +1,75 @@
package qiniucdn_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/qiniu-cdn"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKey string
fSecretKey string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_QINIUCDN_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKey, argsPrefix+"ACCESSKEY", "", "")
flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v qiniu_cdn_test.go -args \
--CERTIMATE_DEPLOYER_QINIUCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_QINIUCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_QINIUCDN_ACCESSKEY="your-access-key" \
--CERTIMATE_DEPLOYER_QINIUCDN_SECRETKEY="your-secret-key" \
--CERTIMATE_DEPLOYER_QINIUCDN_DOMAIN="example.com" \
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("ACCESSKEY: %v", fAccessKey),
fmt.Sprintf("SECRETKEY: %v", fSecretKey),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.New(&provider.QiniuCDNDeployerConfig{
AccessKey: fAccessKey,
SecretKey: fSecretKey,
Domain: fDomain,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@ -0,0 +1,9 @@
package ssh
type OutputFormatType string
const (
OUTPUT_FORMAT_PEM = OutputFormatType("PEM")
OUTPUT_FORMAT_PFX = OutputFormatType("PFX")
OUTPUT_FORMAT_JKS = OutputFormatType("JKS")
)

View File

@ -0,0 +1,252 @@
package ssh
import (
"bytes"
"context"
"errors"
"fmt"
"os"
"path/filepath"
xerrors "github.com/pkg/errors"
"github.com/pkg/sftp"
"golang.org/x/crypto/ssh"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/utils/x509"
)
type SshDeployerConfig struct {
// SSH 主机。
// 零值时默认为 "localhost"。
SshHost string `json:"sshHost,omitempty"`
// SSH 端口。
// 零值时默认为 22。
SshPort int32 `json:"sshPort,omitempty"`
// SSH 登录用户名。
SshUsername string `json:"sshUsername,omitempty"`
// SSH 登录密码。
SshPassword string `json:"sshPassword,omitempty"`
// SSH 登录私钥。
SshKey string `json:"sshKey,omitempty"`
// SSH 登录私钥口令。
SshKeyPassphrase string `json:"sshKeyPassphrase,omitempty"`
// 前置命令。
PreCommand string `json:"preCommand,omitempty"`
// 后置命令。
PostCommand string `json:"postCommand,omitempty"`
// 输出证书格式。
OutputFormat OutputFormatType `json:"outputFormat,omitempty"`
// 输出证书文件路径。
OutputCertPath string `json:"outputCertPath,omitempty"`
// 输出私钥文件路径。
OutputKeyPath string `json:"outputKeyPath,omitempty"`
// PFX 导出密码。
// 证书格式为 PFX 时必填。
PfxPassword string `json:"pfxPassword,omitempty"`
// JKS 别名。
// 证书格式为 JKS 时必填。
JksAlias string `json:"jksAlias,omitempty"`
// JKS 密钥密码。
// 证书格式为 JKS 时必填。
JksKeypass string `json:"jksKeypass,omitempty"`
// JKS 存储密码。
// 证书格式为 JKS 时必填。
JksStorepass string `json:"jksStorepass,omitempty"`
}
type SshDeployer struct {
config *SshDeployerConfig
logger deployer.Logger
}
var _ deployer.Deployer = (*SshDeployer)(nil)
func New(config *SshDeployerConfig) (*SshDeployer, error) {
return NewWithLogger(config, deployer.NewNilLogger())
}
func NewWithLogger(config *SshDeployerConfig, logger deployer.Logger) (*SshDeployer, error) {
if config == nil {
return nil, errors.New("config is nil")
}
if logger == nil {
return nil, errors.New("logger is nil")
}
return &SshDeployer{
logger: logger,
config: config,
}, nil
}
func (d *SshDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
// 连接
client, err := createSshClient(
d.config.SshHost,
d.config.SshPort,
d.config.SshUsername,
d.config.SshPassword,
d.config.SshKey,
d.config.SshKeyPassphrase,
)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssh client")
}
defer client.Close()
d.logger.Logt("SSH connected")
// 执行前置命令
if d.config.PreCommand != "" {
stdout, stderr, err := execSshCommand(client, d.config.PreCommand)
if err != nil {
return nil, xerrors.Wrapf(err, "failed to run pre-command: stdout: %s, stderr: %s", stdout, stderr)
}
d.logger.Logt("SSH pre-command executed", stdout)
}
// 上传证书和私钥文件
switch d.config.OutputFormat {
case OUTPUT_FORMAT_PEM:
if err := writeSftpFileString(client, d.config.OutputCertPath, certPem); err != nil {
return nil, xerrors.Wrap(err, "failed to upload certificate file")
}
d.logger.Logt("certificate file uploaded")
if err := writeSftpFileString(client, d.config.OutputKeyPath, privkeyPem); err != nil {
return nil, xerrors.Wrap(err, "failed to upload private key file")
}
d.logger.Logt("private key file uploaded")
case OUTPUT_FORMAT_PFX:
pfxData, err := x509.TransformCertificateFromPEMToPFX(certPem, privkeyPem, d.config.PfxPassword)
if err != nil {
return nil, xerrors.Wrap(err, "failed to transform certificate to PFX")
}
d.logger.Logt("certificate transformed to PFX")
if err := writeSftpFile(client, d.config.OutputCertPath, pfxData); err != nil {
return nil, xerrors.Wrap(err, "failed to upload certificate file")
}
d.logger.Logt("certificate file uploaded")
case OUTPUT_FORMAT_JKS:
jksData, err := x509.TransformCertificateFromPEMToJKS(certPem, privkeyPem, d.config.JksAlias, d.config.JksKeypass, d.config.JksStorepass)
if err != nil {
return nil, xerrors.Wrap(err, "failed to transform certificate to JKS")
}
d.logger.Logt("certificate transformed to JKS")
if err := writeSftpFile(client, d.config.OutputCertPath, jksData); err != nil {
return nil, xerrors.Wrap(err, "failed to upload certificate file")
}
d.logger.Logt("certificate file uploaded")
default:
return nil, fmt.Errorf("unsupported output format: %s", d.config.OutputFormat)
}
// 执行后置命令
if d.config.PostCommand != "" {
stdout, stderr, err := execSshCommand(client, d.config.PostCommand)
if err != nil {
return nil, xerrors.Wrapf(err, "failed to run command, stdout: %s, stderr: %s", stdout, stderr)
}
d.logger.Logt("SSH post-command executed", stdout)
}
return &deployer.DeployResult{}, nil
}
func createSshClient(host string, port int32, username string, password string, key string, keyPassphrase string) (*ssh.Client, error) {
if host == "" {
host = "localhost"
}
if port == 0 {
port = 22
}
var authMethod ssh.AuthMethod
if key != "" {
var signer ssh.Signer
var err error
if keyPassphrase != "" {
signer, err = ssh.ParsePrivateKeyWithPassphrase([]byte(key), []byte(keyPassphrase))
} else {
signer, err = ssh.ParsePrivateKey([]byte(key))
}
if err != nil {
return nil, err
}
authMethod = ssh.PublicKeys(signer)
} else {
authMethod = ssh.Password(password)
}
return ssh.Dial("tcp", fmt.Sprintf("%s:%d", host, port), &ssh.ClientConfig{
User: username,
Auth: []ssh.AuthMethod{authMethod},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
})
}
func execSshCommand(sshCli *ssh.Client, command string) (string, string, error) {
session, err := sshCli.NewSession()
if err != nil {
return "", "", err
}
defer session.Close()
var stdoutBuf bytes.Buffer
session.Stdout = &stdoutBuf
var stderrBuf bytes.Buffer
session.Stderr = &stderrBuf
err = session.Run(command)
if err != nil {
return "", "", err
}
return stdoutBuf.String(), stderrBuf.String(), nil
}
func writeSftpFileString(sshCli *ssh.Client, path string, content string) error {
return writeSftpFile(sshCli, path, []byte(content))
}
func writeSftpFile(sshCli *ssh.Client, path string, data []byte) error {
sftpCli, err := sftp.NewClient(sshCli)
if err != nil {
return xerrors.Wrap(err, "failed to create sftp client")
}
defer sftpCli.Close()
if err := sftpCli.MkdirAll(filepath.Dir(path)); err != nil {
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 xerrors.Wrap(err, "failed to open remote file")
}
defer file.Close()
_, err = file.Write(data)
if err != nil {
return xerrors.Wrap(err, "failed to write to remote file")
}
return nil
}

View File

@ -0,0 +1,90 @@
package ssh_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ssh"
)
var (
fInputCertPath string
fInputKeyPath string
fSshHost string
fSshPort int
fSshUsername string
fSshPassword string
fOutputCertPath string
fOutputKeyPath string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_SSH_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fSshHost, argsPrefix+"SSHHOST", "", "")
flag.IntVar(&fSshPort, argsPrefix+"SSHPORT", 0, "")
flag.StringVar(&fSshUsername, argsPrefix+"SSHUSERNAME", "", "")
flag.StringVar(&fSshPassword, argsPrefix+"SSHPASSWORD", "", "")
flag.StringVar(&fOutputCertPath, argsPrefix+"OUTPUTCERTPATH", "", "")
flag.StringVar(&fOutputKeyPath, argsPrefix+"OUTPUTKEYPATH", "", "")
}
/*
Shell command to run this test:
go test -v ssh_test.go -args \
--CERTIMATE_DEPLOYER_SSH_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_SSH_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_SSH_SSHHOST="localhost" \
--CERTIMATE_DEPLOYER_SSH_SSHPORT=22 \
--CERTIMATE_DEPLOYER_SSH_SSHUSERNAME="root" \
--CERTIMATE_DEPLOYER_SSH_SSHPASSWORD="password" \
--CERTIMATE_DEPLOYER_SSH_OUTPUTCERTPATH="/path/to/your-output-cert.pem" \
--CERTIMATE_DEPLOYER_SSH_OUTPUTKEYPATH="/path/to/your-output-key.pem"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("SSHHOST: %v", fSshHost),
fmt.Sprintf("SSHPORT: %v", fSshPort),
fmt.Sprintf("SSHUSERNAME: %v", fSshUsername),
fmt.Sprintf("SSHPASSWORD: %v", fSshPassword),
fmt.Sprintf("OUTPUTCERTPATH: %v", fOutputCertPath),
fmt.Sprintf("OUTPUTKEYPATH: %v", fOutputKeyPath),
}, "\n"))
deployer, err := provider.New(&provider.SshDeployerConfig{
SshHost: fSshHost,
SshPort: int32(fSshPort),
SshUsername: fSshUsername,
SshPassword: fSshPassword,
OutputCertPath: fOutputCertPath,
OutputKeyPath: fOutputKeyPath,
})
if err != nil {
t.Errorf("err: %+v", err)
panic(err)
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
panic(err)
}
t.Logf("ok: %v", res)
})
}

View File

@ -0,0 +1,198 @@
package tencentcloudcdn
import (
"context"
"errors"
"strings"
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"
tcSsl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205"
"golang.org/x/exp/slices"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
providerSsl "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/tencentcloud-ssl"
)
type TencentCloudCDNDeployerConfig struct {
// 腾讯云 SecretId。
SecretId string `json:"secretId"`
// 腾讯云 SecretKey。
SecretKey string `json:"secretKey"`
// 加速域名(支持泛域名)。
Domain string `json:"domain"`
}
type TencentCloudCDNDeployer struct {
config *TencentCloudCDNDeployerConfig
logger deployer.Logger
sdkClients *tencentCloudCDNDeployerSdkClients
sslUploader uploader.Uploader
}
var _ deployer.Deployer = (*TencentCloudCDNDeployer)(nil)
type tencentCloudCDNDeployerSdkClients struct {
ssl *tcSsl.Client
cdn *tcCdn.Client
}
func New(config *TencentCloudCDNDeployerConfig) (*TencentCloudCDNDeployer, error) {
return NewWithLogger(config, deployer.NewNilLogger())
}
func NewWithLogger(config *TencentCloudCDNDeployerConfig, logger deployer.Logger) (*TencentCloudCDNDeployer, error) {
if config == nil {
return nil, errors.New("config is nil")
}
if logger == nil {
return nil, errors.New("logger is nil")
}
clients, err := createSdkClients(config.SecretId, config.SecretKey)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk clients")
}
uploader, err := providerSsl.New(&providerSsl.TencentCloudSSLUploaderConfig{
SecretId: config.SecretId,
SecretKey: config.SecretKey,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &TencentCloudCDNDeployer{
logger: logger,
config: config,
sdkClients: clients,
sslUploader: uploader,
}, nil
}
func (d *TencentCloudCDNDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
// 上传证书到 SSL
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
if err != nil {
return nil, xerrors.Wrap(err, "failed to upload certificate file")
}
d.logger.Logt("certificate file uploaded", upres)
// 获取待部署的 CDN 实例
// 如果是泛域名,根据证书匹配 CDN 实例
instanceIds := make([]string, 0)
if strings.HasPrefix(d.config.Domain, "*.") {
domains, err := d.getDomainsByCertificateId(upres.CertId)
if err != nil {
return nil, err
}
instanceIds = domains
} else {
instanceIds = append(instanceIds, d.config.Domain)
}
// 跳过已部署的 CDN 实例
if len(instanceIds) > 0 {
deployedDomains, err := d.getDeployedDomainsByCertificateId(upres.CertId)
if err != nil {
return nil, err
}
temp := make([]string, 0)
for _, instanceId := range instanceIds {
if !slices.Contains(deployedDomains, instanceId) {
temp = append(temp, instanceId)
}
}
instanceIds = temp
}
if len(instanceIds) == 0 {
d.logger.Logt("已部署过或没有要部署的 CDN 实例")
} else {
// 证书部署到 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(instanceIds)
deployCertificateInstanceResp, err := d.sdkClients.ssl.DeployCertificateInstance(deployCertificateInstanceReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'ssl.DeployCertificateInstance'")
}
d.logger.Logt("已部署证书到云资源实例", deployCertificateInstanceResp.Response)
}
return &deployer.DeployResult{}, nil
}
func (d *TencentCloudCDNDeployer) getDomainsByCertificateId(cloudCertId string) ([]string, error) {
// 获取证书中的可用域名
// REF: https://cloud.tencent.com/document/product/228/42491
describeCertDomainsReq := tcCdn.NewDescribeCertDomainsRequest()
describeCertDomainsReq.CertId = common.StringPtr(cloudCertId)
describeCertDomainsReq.Product = common.StringPtr("cdn")
describeCertDomainsResp, err := d.sdkClients.cdn.DescribeCertDomains(describeCertDomainsReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.DescribeCertDomains'")
}
domains := make([]string, 0)
if describeCertDomainsResp.Response.Domains == nil {
for _, domain := range describeCertDomainsResp.Response.Domains {
domains = append(domains, *domain)
}
}
return domains, nil
}
func (d *TencentCloudCDNDeployer) getDeployedDomainsByCertificateId(cloudCertId string) ([]string, error) {
// 根据证书查询关联 CDN 域名
// REF: https://cloud.tencent.com/document/product/400/62674
describeDeployedResourcesReq := tcSsl.NewDescribeDeployedResourcesRequest()
describeDeployedResourcesReq.CertificateIds = common.StringPtrs([]string{cloudCertId})
describeDeployedResourcesReq.ResourceType = common.StringPtr("cdn")
describeDeployedResourcesResp, err := d.sdkClients.ssl.DescribeDeployedResources(describeDeployedResourcesReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.DescribeDeployedResources'")
}
domains := make([]string, 0)
if describeDeployedResourcesResp.Response.DeployedResources != nil {
for _, deployedResource := range describeDeployedResourcesResp.Response.DeployedResources {
for _, resource := range deployedResource.Resources {
domains = append(domains, *resource)
}
}
}
return domains, nil
}
func createSdkClients(secretId, secretKey string) (*tencentCloudCDNDeployerSdkClients, error) {
credential := common.NewCredential(secretId, secretKey)
sslClient, err := tcSsl.NewClient(credential, "", profile.NewClientProfile())
if err != nil {
return nil, err
}
cdnClient, err := tcCdn.NewClient(credential, "", profile.NewClientProfile())
if err != nil {
return nil, err
}
return &tencentCloudCDNDeployerSdkClients{
ssl: sslClient,
cdn: cdnClient,
}, nil
}

View File

@ -0,0 +1,75 @@
package tencentcloudcdn_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-cdn"
)
var (
fInputCertPath string
fInputKeyPath string
fSecretId string
fSecretKey string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_TENCENTCLOUDCDN_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fSecretId, argsPrefix+"SECRETID", "", "")
flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v tencentcloud_cdn_test.go -args \
--CERTIMATE_DEPLOYER_TENCENTCLOUDCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDCDN_SECRETID="your-secret-id" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDCDN_SECRETKEY="your-secret-key" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDCDN_DOMAIN="example.com"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("SECRETID: %v", fSecretId),
fmt.Sprintf("SECRETKEY: %v", fSecretKey),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.New(&provider.TencentCloudCDNDeployerConfig{
SecretId: fSecretId,
SecretKey: fSecretKey,
Domain: fDomain,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@ -0,0 +1,14 @@
package tencentcloudclb
type DeployResourceType string
const (
// 资源类型:通过 SSL 服务部署到云资源实例。
DEPLOY_RESOURCE_USE_SSLDEPLOY = DeployResourceType("ssl-deploy")
// 资源类型:部署到指定负载均衡器。
DEPLOY_RESOURCE_LOADBALANCER = DeployResourceType("loadbalancer")
// 资源类型:部署到指定监听器。
DEPLOY_RESOURCE_LISTENER = DeployResourceType("listener")
// 资源类型:部署到指定转发规则域名。
DEPLOY_RESOURCE_RULEDOMAIN = DeployResourceType("ruledomain")
)

View File

@ -0,0 +1,305 @@
package tencentcloudclb
import (
"context"
"errors"
"fmt"
xerrors "github.com/pkg/errors"
tcClb "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/clb/v20180317"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
tcSsl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
providerSsl "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/tencentcloud-ssl"
)
type TencentCloudCLBDeployerConfig struct {
// 腾讯云 SecretId。
SecretId string `json:"secretId"`
// 腾讯云 SecretKey。
SecretKey string `json:"secretKey"`
// 腾讯云地域。
Region string `json:"region"`
// 部署资源类型。
ResourceType DeployResourceType `json:"resourceType"`
// 负载均衡器 ID。
// 部署资源类型为 [DEPLOY_RESOURCE_SSLDEPLOY]、[DEPLOY_RESOURCE_LOADBALANCER]、[DEPLOY_RESOURCE_RULEDOMAIN] 时必填。
LoadbalancerId string `json:"loadbalancerId,omitempty"`
// 负载均衡监听 ID。
// 部署资源类型为 [DEPLOY_RESOURCE_SSLDEPLOY]、[DEPLOY_RESOURCE_LOADBALANCER]、[DEPLOY_RESOURCE_LISTENER]、[DEPLOY_RESOURCE_RULEDOMAIN] 时必填。
ListenerId string `json:"listenerId,omitempty"`
// SNI 域名或七层转发规则域名(支持泛域名)。
// 部署资源类型为 [DEPLOY_RESOURCE_SSLDEPLOY] 时选填;部署资源类型为 [DEPLOY_RESOURCE_RULEDOMAIN] 时必填。
Domain string `json:"domain,omitempty"`
}
type TencentCloudCLBDeployer struct {
config *TencentCloudCLBDeployerConfig
logger deployer.Logger
sdkClients *wSdkClients
sslUploader uploader.Uploader
}
var _ deployer.Deployer = (*TencentCloudCLBDeployer)(nil)
type wSdkClients struct {
ssl *tcSsl.Client
clb *tcClb.Client
}
func New(config *TencentCloudCLBDeployerConfig) (*TencentCloudCLBDeployer, error) {
return NewWithLogger(config, deployer.NewNilLogger())
}
func NewWithLogger(config *TencentCloudCLBDeployerConfig, logger deployer.Logger) (*TencentCloudCLBDeployer, error) {
if config == nil {
return nil, errors.New("config is nil")
}
if logger == nil {
return nil, errors.New("logger is nil")
}
clients, err := createSdkClients(config.SecretId, config.SecretKey, config.Region)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk clients")
}
uploader, err := providerSsl.New(&providerSsl.TencentCloudSSLUploaderConfig{
SecretId: config.SecretId,
SecretKey: config.SecretKey,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &TencentCloudCLBDeployer{
logger: logger,
config: config,
sdkClients: clients,
sslUploader: uploader,
}, nil
}
func (d *TencentCloudCLBDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
// 上传证书到 SSL
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
if err != nil {
return nil, xerrors.Wrap(err, "failed to upload certificate file")
}
d.logger.Logt("certificate file uploaded", upres)
// 根据部署资源类型决定部署方式
switch d.config.ResourceType {
case DEPLOY_RESOURCE_USE_SSLDEPLOY:
if err := d.deployToInstanceUseSsl(ctx, upres.CertId); err != nil {
return nil, err
}
case DEPLOY_RESOURCE_LOADBALANCER:
if err := d.deployToLoadbalancer(ctx, upres.CertId); err != nil {
return nil, err
}
case DEPLOY_RESOURCE_LISTENER:
if err := d.deployToListener(ctx, upres.CertId); err != nil {
return nil, err
}
case DEPLOY_RESOURCE_RULEDOMAIN:
if err := d.deployToRuleDomain(ctx, upres.CertId); err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unsupported resource type: %s", d.config.ResourceType)
}
return &deployer.DeployResult{}, nil
}
func (d *TencentCloudCLBDeployer) deployToInstanceUseSsl(ctx context.Context, cloudCertId string) error {
if d.config.LoadbalancerId == "" {
return errors.New("config `loadbalancerId` is required")
}
if d.config.ListenerId == "" {
return errors.New("config `listenerId` is required")
}
// 证书部署到 CLB 实例
// REF: https://cloud.tencent.com/document/product/400/91667
deployCertificateInstanceReq := tcSsl.NewDeployCertificateInstanceRequest()
deployCertificateInstanceReq.CertificateId = common.StringPtr(cloudCertId)
deployCertificateInstanceReq.ResourceType = common.StringPtr("clb")
deployCertificateInstanceReq.Status = common.Int64Ptr(1)
if d.config.Domain == "" {
// 未开启 SNI只需指定到监听器
deployCertificateInstanceReq.InstanceIdList = common.StringPtrs([]string{fmt.Sprintf("%s|%s", d.config.LoadbalancerId, d.config.ListenerId)})
} else {
// 开启 SNI需指定到域名支持泛域名
deployCertificateInstanceReq.InstanceIdList = common.StringPtrs([]string{fmt.Sprintf("%s|%s|%s", d.config.LoadbalancerId, d.config.ListenerId, d.config.Domain)})
}
deployCertificateInstanceResp, err := d.sdkClients.ssl.DeployCertificateInstance(deployCertificateInstanceReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'ssl.DeployCertificateInstance'")
}
d.logger.Logt("已部署证书到云资源实例", deployCertificateInstanceResp.Response)
return nil
}
func (d *TencentCloudCLBDeployer) deployToLoadbalancer(ctx context.Context, cloudCertId string) error {
if d.config.LoadbalancerId == "" {
return errors.New("config `loadbalancerId` is required")
}
listenerIds := make([]string, 0)
// 查询监听器列表
// REF: https://cloud.tencent.com/document/api/214/30686
describeListenersReq := tcClb.NewDescribeListenersRequest()
describeListenersReq.LoadBalancerId = common.StringPtr(d.config.LoadbalancerId)
describeListenersResp, err := d.sdkClients.clb.DescribeListeners(describeListenersReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'clb.DescribeListeners'")
} else {
if describeListenersResp.Response.Listeners != nil {
for _, listener := range describeListenersResp.Response.Listeners {
if listener.Protocol == nil || (*listener.Protocol != "HTTPS" && *listener.Protocol != "TCP_SSL" && *listener.Protocol != "QUIC") {
continue
}
listenerIds = append(listenerIds, *listener.ListenerId)
}
}
}
d.logger.Logt("已查询到负载均衡器下的监听器", listenerIds)
// 批量更新监听器证书
if len(listenerIds) > 0 {
var errs []error
for _, listenerId := range listenerIds {
if err := d.modifyListenerCertificate(ctx, d.config.LoadbalancerId, listenerId, cloudCertId); err != nil {
errs = append(errs, err)
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
}
return nil
}
func (d *TencentCloudCLBDeployer) deployToListener(ctx context.Context, cloudCertId string) error {
if d.config.LoadbalancerId == "" {
return errors.New("config `loadbalancerId` is required")
}
if d.config.ListenerId == "" {
return errors.New("config `listenerId` is required")
}
// 更新监听器证书
if err := d.modifyListenerCertificate(ctx, d.config.LoadbalancerId, d.config.ListenerId, cloudCertId); err != nil {
return err
}
return nil
}
func (d *TencentCloudCLBDeployer) deployToRuleDomain(ctx context.Context, cloudCertId string) error {
if d.config.LoadbalancerId == "" {
return errors.New("config `loadbalancerId` is required")
}
if d.config.ListenerId == "" {
return errors.New("config `listenerId` is required")
}
if d.config.Domain == "" {
return errors.New("config `domain` is required")
}
// 修改负载均衡七层监听器转发规则的域名级别属性
// REF: https://cloud.tencent.com/document/api/214/38092
modifyDomainAttributesReq := tcClb.NewModifyDomainAttributesRequest()
modifyDomainAttributesReq.LoadBalancerId = common.StringPtr(d.config.LoadbalancerId)
modifyDomainAttributesReq.ListenerId = common.StringPtr(d.config.ListenerId)
modifyDomainAttributesReq.Domain = common.StringPtr(d.config.Domain)
modifyDomainAttributesReq.Certificate = &tcClb.CertificateInput{
SSLMode: common.StringPtr("UNIDIRECTIONAL"),
CertId: common.StringPtr(cloudCertId),
}
modifyDomainAttributesResp, err := d.sdkClients.clb.ModifyDomainAttributes(modifyDomainAttributesReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'clb.ModifyDomainAttributes'")
}
d.logger.Logt("已修改七层监听器转发规则的域名级别属性", modifyDomainAttributesResp.Response)
return nil
}
func (d *TencentCloudCLBDeployer) modifyListenerCertificate(ctx context.Context, cloudLoadbalancerId, cloudListenerId, cloudCertId string) error {
// 查询监听器列表
// REF: https://cloud.tencent.com/document/api/214/30686
describeListenersReq := tcClb.NewDescribeListenersRequest()
describeListenersReq.LoadBalancerId = common.StringPtr(cloudLoadbalancerId)
describeListenersReq.ListenerIds = common.StringPtrs([]string{cloudListenerId})
describeListenersResp, err := d.sdkClients.clb.DescribeListeners(describeListenersReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'clb.DescribeListeners'")
}
if len(describeListenersResp.Response.Listeners) == 0 {
return errors.New("listener not found")
}
d.logger.Logt("已查询到监听器属性", describeListenersResp.Response)
// 修改监听器属性
// REF: https://cloud.tencent.com/document/product/214/30681
modifyListenerReq := tcClb.NewModifyListenerRequest()
modifyListenerReq.LoadBalancerId = common.StringPtr(cloudLoadbalancerId)
modifyListenerReq.ListenerId = common.StringPtr(cloudListenerId)
modifyListenerReq.Certificate = &tcClb.CertificateInput{CertId: common.StringPtr(cloudCertId)}
if describeListenersResp.Response.Listeners[0].Certificate != nil && describeListenersResp.Response.Listeners[0].Certificate.SSLMode != nil {
modifyListenerReq.Certificate.SSLMode = describeListenersResp.Response.Listeners[0].Certificate.SSLMode
modifyListenerReq.Certificate.CertCaId = describeListenersResp.Response.Listeners[0].Certificate.CertCaId
} else {
modifyListenerReq.Certificate.SSLMode = common.StringPtr("UNIDIRECTIONAL")
}
modifyListenerResp, err := d.sdkClients.clb.ModifyListener(modifyListenerReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'clb.ModifyListener'")
}
d.logger.Logt("已修改监听器属性", modifyListenerResp.Response)
return nil
}
func createSdkClients(secretId, secretKey, region string) (*wSdkClients, error) {
credential := common.NewCredential(secretId, secretKey)
// 注意虽然官方文档中地域无需指定,但实际需要部署到 CLB 时必传
sslClient, err := tcSsl.NewClient(credential, region, profile.NewClientProfile())
if err != nil {
return nil, err
}
clbClient, err := tcClb.NewClient(credential, region, profile.NewClientProfile())
if err != nil {
return nil, err
}
return &wSdkClients{
ssl: sslClient,
clb: clbClient,
}, nil
}

View File

@ -0,0 +1,199 @@
package tencentcloudclb_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-clb"
)
var (
fInputCertPath string
fInputKeyPath string
fSecretId string
fSecretKey string
fRegion string
fLoadbalancerId string
fListenerId string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_TENCENTCLOUDCDN_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fSecretId, argsPrefix+"SECRETID", "", "")
flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "")
flag.StringVar(&fRegion, argsPrefix+"REGION", "", "")
flag.StringVar(&fLoadbalancerId, argsPrefix+"LOADBALANCERID", "", "")
flag.StringVar(&fListenerId, argsPrefix+"LISTENERID", "", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v tencentcloud_clb_test.go -args \
--CERTIMATE_DEPLOYER_TENCENTCLOUDCLB_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDCLB_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDCLB_SECRETID="your-secret-id" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDCLB_SECRETKEY="your-secret-key" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDCLB_REGION="ap-guangzhou" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDCLB_LOADBALANCERID="your-clb-lb-id" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDCLB_LISTENERID="your-clb-lbl-id" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDCLB_DOMAIN="example.com"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy_UseSslDeploy", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("SECRETID: %v", fSecretId),
fmt.Sprintf("SECRETKEY: %v", fSecretKey),
fmt.Sprintf("REGION: %v", fRegion),
fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId),
fmt.Sprintf("LISTENERID: %v", fListenerId),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.New(&provider.TencentCloudCLBDeployerConfig{
SecretId: fSecretId,
SecretKey: fSecretKey,
Region: fRegion,
ResourceType: provider.DEPLOY_RESOURCE_USE_SSLDEPLOY,
LoadbalancerId: fLoadbalancerId,
ListenerId: fListenerId,
Domain: fDomain,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
t.Run("Deploy_ToLoadbalancer", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("SECRETID: %v", fSecretId),
fmt.Sprintf("SECRETKEY: %v", fSecretKey),
fmt.Sprintf("REGION: %v", fRegion),
fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId),
}, "\n"))
deployer, err := provider.New(&provider.TencentCloudCLBDeployerConfig{
SecretId: fSecretId,
SecretKey: fSecretKey,
Region: fRegion,
ResourceType: provider.DEPLOY_RESOURCE_LOADBALANCER,
LoadbalancerId: fLoadbalancerId,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
t.Run("Deploy_ToListener", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("SECRETID: %v", fSecretId),
fmt.Sprintf("SECRETKEY: %v", fSecretKey),
fmt.Sprintf("REGION: %v", fRegion),
fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId),
fmt.Sprintf("LISTENERID: %v", fListenerId),
}, "\n"))
deployer, err := provider.New(&provider.TencentCloudCLBDeployerConfig{
SecretId: fSecretId,
SecretKey: fSecretKey,
Region: fRegion,
ResourceType: provider.DEPLOY_RESOURCE_LISTENER,
LoadbalancerId: fLoadbalancerId,
ListenerId: fListenerId,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
t.Run("Deploy_ToRuleDomain", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("SECRETID: %v", fSecretId),
fmt.Sprintf("SECRETKEY: %v", fSecretKey),
fmt.Sprintf("REGION: %v", fRegion),
fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId),
fmt.Sprintf("LISTENERID: %v", fListenerId),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.New(&provider.TencentCloudCLBDeployerConfig{
SecretId: fSecretId,
SecretKey: fSecretKey,
Region: fRegion,
ResourceType: provider.DEPLOY_RESOURCE_RULEDOMAIN,
LoadbalancerId: fLoadbalancerId,
ListenerId: fListenerId,
Domain: fDomain,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@ -0,0 +1,115 @@
package tencentcloudcdn
import (
"context"
"errors"
"fmt"
xerrors "github.com/pkg/errors"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
tcSsl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
providerSsl "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/tencentcloud-ssl"
)
type TencentCloudCOSDeployerConfig struct {
// 腾讯云 SecretId。
SecretId string `json:"secretId"`
// 腾讯云 SecretKey。
SecretKey string `json:"secretKey"`
// 腾讯云地域。
Region string `json:"region"`
// 存储桶名。
Bucket string `json:"bucket"`
// 自定义域名(不支持泛域名)。
Domain string `json:"domain"`
}
type TencentCloudCOSDeployer struct {
config *TencentCloudCOSDeployerConfig
logger deployer.Logger
sdkClient *tcSsl.Client
sslUploader uploader.Uploader
}
var _ deployer.Deployer = (*TencentCloudCOSDeployer)(nil)
func New(config *TencentCloudCOSDeployerConfig) (*TencentCloudCOSDeployer, error) {
return NewWithLogger(config, deployer.NewNilLogger())
}
func NewWithLogger(config *TencentCloudCOSDeployerConfig, logger deployer.Logger) (*TencentCloudCOSDeployer, error) {
if config == nil {
return nil, errors.New("config is nil")
}
if logger == nil {
return nil, errors.New("logger is nil")
}
client, err := createSdkClient(config.SecretId, config.SecretKey, config.Region)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk clients")
}
uploader, err := providerSsl.New(&providerSsl.TencentCloudSSLUploaderConfig{
SecretId: config.SecretId,
SecretKey: config.SecretKey,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &TencentCloudCOSDeployer{
logger: logger,
config: config,
sdkClient: client,
sslUploader: uploader,
}, nil
}
func (d *TencentCloudCOSDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
if d.config.Bucket == "" {
return nil, errors.New("config `bucket` is required")
}
if d.config.Domain == "" {
return nil, errors.New("config `domain` is required")
}
// 上传证书到 SSL
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
if err != nil {
return nil, xerrors.Wrap(err, "failed to upload certificate file")
}
d.logger.Logt("certificate file uploaded", upres)
// 证书部署到 COS 实例
// 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", d.config.Region, d.config.Bucket, d.config.Domain)})
deployCertificateInstanceResp, err := d.sdkClient.DeployCertificateInstance(deployCertificateInstanceReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'ssl.DeployCertificateInstance'")
}
d.logger.Logt("已部署证书到云资源实例", deployCertificateInstanceResp.Response)
return &deployer.DeployResult{}, nil
}
func createSdkClient(secretId, secretKey, region string) (*tcSsl.Client, error) {
credential := common.NewCredential(secretId, secretKey)
client, err := tcSsl.NewClient(credential, region, profile.NewClientProfile())
if err != nil {
return nil, err
}
return client, nil
}

View File

@ -0,0 +1,85 @@
package tencentcloudcdn_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-cos"
)
var (
fInputCertPath string
fInputKeyPath string
fSecretId string
fSecretKey string
fRegion string
fBucket string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_TENCENTCLOUDCOS_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fSecretId, argsPrefix+"SECRETID", "", "")
flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "")
flag.StringVar(&fRegion, argsPrefix+"REGION", "", "")
flag.StringVar(&fBucket, argsPrefix+"BUCKET", "", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v tencentcloud_cos_test.go -args \
--CERTIMATE_DEPLOYER_TENCENTCLOUDCOS_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDCOS_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDCOS_SECRETID="your-secret-id" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDCOS_SECRETKEY="your-secret-key" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDCOS_REGION="ap-guangzhou" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDCOS_BUCKET="your-cos-bucket" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDCOS_DOMAIN="example.com"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("SECRETID: %v", fSecretId),
fmt.Sprintf("SECRETKEY: %v", fSecretKey),
fmt.Sprintf("REGION: %v", fRegion),
fmt.Sprintf("BUCKET: %v", fBucket),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.New(&provider.TencentCloudCOSDeployerConfig{
SecretId: fSecretId,
SecretKey: fSecretKey,
Region: fRegion,
Bucket: fBucket,
Domain: fDomain,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@ -0,0 +1,158 @@
package tencentcloudecdn
import (
"context"
"errors"
"strings"
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"
tcSsl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
providerSsl "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/tencentcloud-ssl"
)
type TencentCloudECDNDeployerConfig struct {
// 腾讯云 SecretId。
SecretId string `json:"secretId"`
// 腾讯云 SecretKey。
SecretKey string `json:"secretKey"`
// 加速域名(支持泛域名)。
Domain string `json:"domain"`
}
type TencentCloudECDNDeployer struct {
config *TencentCloudECDNDeployerConfig
logger deployer.Logger
sdkClients *wSdkClients
sslUploader uploader.Uploader
}
var _ deployer.Deployer = (*TencentCloudECDNDeployer)(nil)
type wSdkClients struct {
ssl *tcSsl.Client
cdn *tcCdn.Client
}
func New(config *TencentCloudECDNDeployerConfig) (*TencentCloudECDNDeployer, error) {
return NewWithLogger(config, deployer.NewNilLogger())
}
func NewWithLogger(config *TencentCloudECDNDeployerConfig, logger deployer.Logger) (*TencentCloudECDNDeployer, error) {
if config == nil {
return nil, errors.New("config is nil")
}
if logger == nil {
return nil, errors.New("logger is nil")
}
clients, err := createSdkClients(config.SecretId, config.SecretKey)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk clients")
}
uploader, err := providerSsl.New(&providerSsl.TencentCloudSSLUploaderConfig{
SecretId: config.SecretId,
SecretKey: config.SecretKey,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &TencentCloudECDNDeployer{
logger: logger,
config: config,
sdkClients: clients,
sslUploader: uploader,
}, nil
}
func (d *TencentCloudECDNDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
// 上传证书到 SSL
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
if err != nil {
return nil, xerrors.Wrap(err, "failed to upload certificate file")
}
d.logger.Logt("certificate file uploaded", upres)
// 获取待部署的 CDN 实例
// 如果是泛域名,根据证书匹配 CDN 实例
instanceIds := make([]string, 0)
if strings.HasPrefix(d.config.Domain, "*.") {
domains, err := d.getDomainsByCertificateId(upres.CertId)
if err != nil {
return nil, err
}
instanceIds = domains
} else {
instanceIds = append(instanceIds, d.config.Domain)
}
if len(instanceIds) == 0 {
d.logger.Logt("已部署过或没有要部署的 ECDN 实例")
} else {
// 证书部署到 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(instanceIds)
deployCertificateInstanceResp, err := d.sdkClients.ssl.DeployCertificateInstance(deployCertificateInstanceReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'ssl.DeployCertificateInstance'")
}
d.logger.Logt("已部署证书到云资源实例", deployCertificateInstanceResp.Response)
}
return &deployer.DeployResult{}, nil
}
func (d *TencentCloudECDNDeployer) getDomainsByCertificateId(cloudCertId string) ([]string, error) {
// 获取证书中的可用域名
// REF: https://cloud.tencent.com/document/product/228/42491
describeCertDomainsReq := tcCdn.NewDescribeCertDomainsRequest()
describeCertDomainsReq.CertId = common.StringPtr(cloudCertId)
describeCertDomainsReq.Product = common.StringPtr("ecdn")
describeCertDomainsResp, err := d.sdkClients.cdn.DescribeCertDomains(describeCertDomainsReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.DescribeCertDomains'")
}
domains := make([]string, 0)
if describeCertDomainsResp.Response.Domains == nil {
for _, domain := range describeCertDomainsResp.Response.Domains {
domains = append(domains, *domain)
}
}
return domains, nil
}
func createSdkClients(secretId, secretKey string) (*wSdkClients, error) {
credential := common.NewCredential(secretId, secretKey)
sslClient, err := tcSsl.NewClient(credential, "", profile.NewClientProfile())
if err != nil {
return nil, err
}
cdnClient, err := tcCdn.NewClient(credential, "", profile.NewClientProfile())
if err != nil {
return nil, err
}
return &wSdkClients{
ssl: sslClient,
cdn: cdnClient,
}, nil
}

View File

@ -0,0 +1,75 @@
package tencentcloudecdn_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-ecdn"
)
var (
fInputCertPath string
fInputKeyPath string
fSecretId string
fSecretKey string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_TENCENTCLOUDECDN_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fSecretId, argsPrefix+"SECRETID", "", "")
flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v tencentcloud_ecdn_test.go -args \
--CERTIMATE_DEPLOYER_TENCENTCLOUDECDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDECDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDECDN_SECRETID="your-secret-id" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDECDN_SECRETKEY="your-secret-key" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDECDN_DOMAIN="example.com"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("SECRETID: %v", fSecretId),
fmt.Sprintf("SECRETKEY: %v", fSecretKey),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.New(&provider.TencentCloudECDNDeployerConfig{
SecretId: fSecretId,
SecretKey: fSecretKey,
Domain: fDomain,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@ -0,0 +1,124 @@
package tencentcloudeteo
import (
"context"
"errors"
xerrors "github.com/pkg/errors"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
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/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
providerSsl "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/tencentcloud-ssl"
)
type TencentCloudTEODeployerConfig struct {
// 腾讯云 SecretId。
SecretId string `json:"secretId"`
// 腾讯云 SecretKey。
SecretKey string `json:"secretKey"`
// 站点 ID。
ZoneId string `json:"zoneId"`
// 加速域名(不支持泛域名)。
Domain string `json:"domain"`
}
type TencentCloudTEODeployer struct {
config *TencentCloudTEODeployerConfig
logger deployer.Logger
sdkClients *wSdkClients
sslUploader uploader.Uploader
}
var _ deployer.Deployer = (*TencentCloudTEODeployer)(nil)
type wSdkClients struct {
ssl *tcSsl.Client
teo *tcTeo.Client
}
func New(config *TencentCloudTEODeployerConfig) (*TencentCloudTEODeployer, error) {
return NewWithLogger(config, deployer.NewNilLogger())
}
func NewWithLogger(config *TencentCloudTEODeployerConfig, logger deployer.Logger) (*TencentCloudTEODeployer, error) {
if config == nil {
return nil, errors.New("config is nil")
}
if logger == nil {
return nil, errors.New("logger is nil")
}
clients, err := createSdkClients(config.SecretId, config.SecretKey)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk clients")
}
uploader, err := providerSsl.New(&providerSsl.TencentCloudSSLUploaderConfig{
SecretId: config.SecretId,
SecretKey: config.SecretKey,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &TencentCloudTEODeployer{
logger: logger,
config: config,
sdkClients: clients,
sslUploader: uploader,
}, nil
}
func (d *TencentCloudTEODeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
if d.config.ZoneId == "" {
return nil, xerrors.New("config `zoneId` is required")
}
// 上传证书到 SSL
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
if err != nil {
return nil, xerrors.Wrap(err, "failed to upload certificate file")
}
d.logger.Logt("certificate file uploaded", upres)
// 配置域名证书
// REF: https://cloud.tencent.com/document/product/1552/80764
modifyHostsCertificateReq := tcTeo.NewModifyHostsCertificateRequest()
modifyHostsCertificateReq.ZoneId = common.StringPtr(d.config.ZoneId)
modifyHostsCertificateReq.Mode = common.StringPtr("sslcert")
modifyHostsCertificateReq.Hosts = common.StringPtrs([]string{d.config.Domain})
modifyHostsCertificateReq.ServerCertInfo = []*tcTeo.ServerCertInfo{{CertId: common.StringPtr(upres.CertId)}}
modifyHostsCertificateResp, err := d.sdkClients.teo.ModifyHostsCertificate(modifyHostsCertificateReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'teo.ModifyHostsCertificate'")
}
d.logger.Logt("已配置域名证书", modifyHostsCertificateResp.Response)
return &deployer.DeployResult{}, nil
}
func createSdkClients(secretId, secretKey string) (*wSdkClients, error) {
credential := common.NewCredential(secretId, secretKey)
sslClient, err := tcSsl.NewClient(credential, "", profile.NewClientProfile())
if err != nil {
return nil, err
}
teoClient, err := tcTeo.NewClient(credential, "", profile.NewClientProfile())
if err != nil {
return nil, err
}
return &wSdkClients{
ssl: sslClient,
teo: teoClient,
}, nil
}

View File

@ -0,0 +1,80 @@
package tencentcloudeteo_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-teo"
)
var (
fInputCertPath string
fInputKeyPath string
fSecretId string
fSecretKey string
fZoneId string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_TENCENTCLOUDETEO_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fSecretId, argsPrefix+"SECRETID", "", "")
flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "")
flag.StringVar(&fZoneId, argsPrefix+"ZONEID", "", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v tencentcloud_cdn_test.go -args \
--CERTIMATE_DEPLOYER_TENCENTCLOUDETEO_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDETEO_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDETEO_SECRETID="your-secret-id" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDETEO_SECRETKEY="your-secret-key" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDETEO_ZONEID="your-zone-id" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDETEO_DOMAIN="example.com"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("SECRETID: %v", fSecretId),
fmt.Sprintf("SECRETKEY: %v", fSecretKey),
fmt.Sprintf("ZONEID: %v", fZoneId),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.New(&provider.TencentCloudTEODeployerConfig{
SecretId: fSecretId,
SecretKey: fSecretKey,
ZoneId: fZoneId,
Domain: fDomain,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@ -0,0 +1,136 @@
package volcenginecdn
import (
"context"
"errors"
"fmt"
"strings"
xerrors "github.com/pkg/errors"
veCdn "github.com/volcengine/volc-sdk-golang/service/cdn"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
providerCdn "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/volcengine-cdn"
)
type VolcEngineCDNDeployerConfig struct {
// 火山引擎 AccessKey。
AccessKey string `json:"accessKey"`
// 火山引擎 SecretKey。
SecretKey string `json:"secretKey"`
// 加速域名(支持泛域名)。
Domain string `json:"domain"`
}
type VolcEngineCDNDeployer struct {
config *VolcEngineCDNDeployerConfig
logger deployer.Logger
sdkClient *veCdn.CDN
sslUploader uploader.Uploader
}
var _ deployer.Deployer = (*VolcEngineCDNDeployer)(nil)
func New(config *VolcEngineCDNDeployerConfig) (*VolcEngineCDNDeployer, error) {
return NewWithLogger(config, deployer.NewNilLogger())
}
func NewWithLogger(config *VolcEngineCDNDeployerConfig, logger deployer.Logger) (*VolcEngineCDNDeployer, error) {
if config == nil {
return nil, errors.New("config is nil")
}
if logger == nil {
return nil, errors.New("logger is nil")
}
client := veCdn.NewInstance()
client.Client.SetAccessKey(config.AccessKey)
client.Client.SetSecretKey(config.SecretKey)
uploader, err := providerCdn.New(&providerCdn.VolcEngineCDNUploaderConfig{
AccessKeyId: config.AccessKey,
AccessKeySecret: config.SecretKey,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &VolcEngineCDNDeployer{
logger: logger,
config: config,
sdkClient: client,
sslUploader: uploader,
}, nil
}
func (d *VolcEngineCDNDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
// 上传证书到 CDN
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
if err != nil {
return nil, xerrors.Wrap(err, "failed to upload certificate file")
}
d.logger.Logt("certificate file uploaded", upres)
domains := make([]string, 0)
if strings.HasPrefix(d.config.Domain, "*.") {
// 获取指定证书可关联的域名
// REF: https://www.volcengine.com/docs/6454/125711
describeCertConfigReq := &veCdn.DescribeCertConfigRequest{
CertId: upres.CertId,
}
describeCertConfigResp, err := d.sdkClient.DescribeCertConfig(describeCertConfigReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.DescribeCertConfig'")
}
if describeCertConfigResp.Result.CertNotConfig != nil {
for i := range describeCertConfigResp.Result.CertNotConfig {
domains = append(domains, describeCertConfigResp.Result.CertNotConfig[i].Domain)
}
}
if describeCertConfigResp.Result.OtherCertConfig != nil {
for i := range describeCertConfigResp.Result.OtherCertConfig {
domains = append(domains, describeCertConfigResp.Result.OtherCertConfig[i].Domain)
}
}
if len(domains) == 0 {
if len(describeCertConfigResp.Result.SpecifiedCertConfig) > 0 {
// 所有可关联的域名都配置了该证书,跳过部署
} else {
return nil, xerrors.New("domain not found")
}
}
} else {
domains = append(domains, d.config.Domain)
}
if len(domains) > 0 {
var errs []error
for _, domain := range domains {
// 关联证书与加速域名
// REF: https://www.volcengine.com/docs/6454/125712
batchDeployCertReq := &veCdn.BatchDeployCertRequest{
CertId: upres.CertId,
Domain: domain,
}
batchDeployCertResp, err := d.sdkClient.BatchDeployCert(batchDeployCertReq)
if err != nil {
errs = append(errs, err)
} else {
d.logger.Logt(fmt.Sprintf("已关联证书到域名 %s", domain), batchDeployCertResp)
}
}
if len(errs) > 0 {
return nil, errors.Join(errs...)
}
}
return &deployer.DeployResult{}, nil
}

View File

@ -0,0 +1,75 @@
package volcenginecdn_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-cdn"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKey string
fSecretKey string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_VOLCENGINECDN_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKey, argsPrefix+"ACCESSKEY", "", "")
flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v volcengine_cdn_test.go -args \
--CERTIMATE_DEPLOYER_VOLCENGINECDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_VOLCENGINECDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_VOLCENGINECDN_ACCESSKEY="your-access-key" \
--CERTIMATE_DEPLOYER_VOLCENGINECDN_SECRETKEY="your-secret-key" \
--CERTIMATE_DEPLOYER_VOLCENGINECDN_DOMAIN="example.com"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("ACCESSKEY: %v", fAccessKey),
fmt.Sprintf("SECRETKEY: %v", fSecretKey),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.New(&provider.VolcEngineCDNDeployerConfig{
AccessKey: fAccessKey,
SecretKey: fSecretKey,
Domain: fDomain,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@ -0,0 +1,146 @@
package volcenginelive
import (
"context"
"errors"
"fmt"
"strings"
xerrors "github.com/pkg/errors"
veLive "github.com/volcengine/volc-sdk-golang/service/live/v20230101"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
providerLive "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/volcengine-live"
"github.com/usual2970/certimate/internal/pkg/utils/cast"
)
type VolcEngineLiveDeployerConfig struct {
// 火山引擎 AccessKey。
AccessKey string `json:"accessKey"`
// 火山引擎 SecretKey。
SecretKey string `json:"secretKey"`
// 加速域名(支持泛域名)。
Domain string `json:"domain"`
}
type VolcEngineLiveDeployer struct {
config *VolcEngineLiveDeployerConfig
logger deployer.Logger
sdkClient *veLive.Live
sslUploader uploader.Uploader
}
var _ deployer.Deployer = (*VolcEngineLiveDeployer)(nil)
func New(config *VolcEngineLiveDeployerConfig) (*VolcEngineLiveDeployer, error) {
return NewWithLogger(config, deployer.NewNilLogger())
}
func NewWithLogger(config *VolcEngineLiveDeployerConfig, logger deployer.Logger) (*VolcEngineLiveDeployer, error) {
if config == nil {
return nil, errors.New("config is nil")
}
if logger == nil {
return nil, errors.New("logger is nil")
}
client := veLive.NewInstance()
client.SetAccessKey(config.AccessKey)
client.SetSecretKey(config.SecretKey)
uploader, err := providerLive.New(&providerLive.VolcEngineLiveUploaderConfig{
AccessKeyId: config.AccessKey,
AccessKeySecret: config.SecretKey,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &VolcEngineLiveDeployer{
logger: logger,
config: config,
sdkClient: client,
sslUploader: uploader,
}, nil
}
func (d *VolcEngineLiveDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
// 上传证书到 Live
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
if err != nil {
return nil, xerrors.Wrap(err, "failed to upload certificate file")
}
d.logger.Logt("certificate file uploaded", upres)
domains := make([]string, 0)
if strings.HasPrefix(d.config.Domain, "*.") {
listDomainDetailPageNum := int32(1)
listDomainDetailPageSize := int32(1000)
listDomainDetailTotal := 0
for {
// 查询域名列表
// REF: https://www.volcengine.com/docs/6469/1186277#%E6%9F%A5%E8%AF%A2%E5%9F%9F%E5%90%8D%E5%88%97%E8%A1%A8
listDomainDetailReq := &veLive.ListDomainDetailBody{
PageNum: listDomainDetailPageNum,
PageSize: listDomainDetailPageSize,
}
listDomainDetailResp, err := d.sdkClient.ListDomainDetail(ctx, listDomainDetailReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'live.ListDomainDetail'")
}
if listDomainDetailResp.Result.DomainList != nil {
for _, item := range listDomainDetailResp.Result.DomainList {
// 仅匹配泛域名的下一级子域名
wildcardDomain := strings.TrimPrefix(d.config.Domain, "*")
if strings.HasSuffix(item.Domain, wildcardDomain) && !strings.Contains(strings.TrimSuffix(item.Domain, wildcardDomain), ".") {
domains = append(domains, item.Domain)
}
}
}
listDomainDetailLen := len(listDomainDetailResp.Result.DomainList)
if listDomainDetailLen < int(listDomainDetailPageSize) || int(listDomainDetailResp.Result.Total) <= listDomainDetailTotal+listDomainDetailLen {
break
} else {
listDomainDetailPageNum++
listDomainDetailTotal += listDomainDetailLen
}
}
if len(domains) == 0 {
return nil, xerrors.Errorf("未查询到匹配的域名: %s", d.config.Domain)
}
} else {
domains = append(domains, d.config.Domain)
}
if len(domains) > 0 {
var errs []error
for _, domain := range domains {
// 绑定证书
// REF: https://www.volcengine.com/docs/6469/1186278#%E7%BB%91%E5%AE%9A%E8%AF%81%E4%B9%A6
bindCertReq := &veLive.BindCertBody{
ChainID: upres.CertId,
Domain: domain,
HTTPS: cast.BoolPtr(true),
}
bindCertResp, err := d.sdkClient.BindCert(ctx, bindCertReq)
if err != nil {
errs = append(errs, err)
} else {
d.logger.Logt(fmt.Sprintf("已绑定证书到域名 %s", domain), bindCertResp)
}
}
if len(errs) > 0 {
return nil, errors.Join(errs...)
}
}
return &deployer.DeployResult{}, nil
}

View File

@ -0,0 +1,75 @@
package volcenginelive_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-live"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKey string
fSecretKey string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_VOLCENGINELIVE_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKey, argsPrefix+"ACCESSKEY", "", "")
flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v volcengine_live_test.go -args \
--CERTIMATE_DEPLOYER_VOLCENGINELIVE_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_VOLCENGINELIVE_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_VOLCENGINELIVE_ACCESSKEY="your-access-key" \
--CERTIMATE_DEPLOYER_VOLCENGINELIVE_SECRETKEY="your-secret-key" \
--CERTIMATE_DEPLOYER_VOLCENGINELIVE_DOMAIN="example.com"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("ACCESSKEY: %v", fAccessKey),
fmt.Sprintf("SECRETKEY: %v", fSecretKey),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.New(&provider.VolcEngineLiveDeployerConfig{
AccessKey: fAccessKey,
SecretKey: fSecretKey,
Domain: fDomain,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@ -0,0 +1,85 @@
package webhook
import (
"bytes"
"context"
"encoding/json"
"errors"
"net/http"
"strings"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/utils/x509"
xhttp "github.com/usual2970/certimate/internal/utils/http"
)
type WebhookDeployerConfig struct {
// Webhook URL。
Url string `json:"url"`
// Webhook 变量字典。
Variables map[string]string `json:"variables,omitempty"`
}
type WebhookDeployer struct {
config *WebhookDeployerConfig
logger deployer.Logger
}
var _ deployer.Deployer = (*WebhookDeployer)(nil)
func New(config *WebhookDeployerConfig) (*WebhookDeployer, error) {
return NewWithLogger(config, deployer.NewNilLogger())
}
func NewWithLogger(config *WebhookDeployerConfig, logger deployer.Logger) (*WebhookDeployer, error) {
if config == nil {
return nil, errors.New("config is nil")
}
if logger == nil {
return nil, errors.New("logger is nil")
}
return &WebhookDeployer{
config: config,
logger: logger,
}, nil
}
type webhookData struct {
SubjectAltNames string `json:"subjectAltNames"`
Certificate string `json:"certificate"`
PrivateKey string `json:"privateKey"`
Variables map[string]string `json:"variables"`
}
func (d *WebhookDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
certX509, err := x509.ParseCertificateFromPEM(certPem)
if err != nil {
return nil, xerrors.Wrap(err, "failed to parse x509")
}
data := &webhookData{
SubjectAltNames: strings.Join(certX509.DNSNames, ","),
Certificate: certPem,
PrivateKey: privkeyPem,
Variables: d.config.Variables,
}
body, _ := json.Marshal(data)
resp, err := xhttp.Req(d.config.Url, http.MethodPost, bytes.NewReader(body), map[string]string{
"Content-Type": "application/json",
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to send webhook request")
}
d.logger.Logt("Webhook Response", string(resp))
return &deployer.DeployResult{
DeploymentData: map[string]any{
"responseText": string(resp),
},
}, nil
}

View File

@ -0,0 +1,65 @@
package webhook_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/webhook"
)
var (
fInputCertPath string
fInputKeyPath string
fUrl string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_WEBHOOK_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fUrl, argsPrefix+"URL", "", "")
}
/*
Shell command to run this test:
go test -v webhook_test.go -args \
--CERTIMATE_DEPLOYER_WEBHOOK_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_WEBHOOK_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_WEBHOOK_URL="https://example.com/your-webhook-url"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("URL: %v", fUrl),
}, "\n"))
deployer, err := provider.New(&provider.WebhookDeployerConfig{
Url: fUrl,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@ -0,0 +1,64 @@
package bark_test
import (
"context"
"flag"
"fmt"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/bark"
)
const (
mockSubject = "test_subject"
mockMessage = "test_message"
)
var (
fServerUrl string
fDeviceKey string
)
func init() {
argsPrefix := "CERTIMATE_NOTIFIER_BARK_"
flag.StringVar(&fServerUrl, argsPrefix+"SERVERURL", "", "")
flag.StringVar(&fDeviceKey, argsPrefix+"DEVICEKEY", "", "")
}
/*
Shell command to run this test:
go test -v bark_test.go -args \
--CERTIMATE_NOTIFIER_BARK_SERVERURL="https://example.com/your-server-url" \
--CERTIMATE_NOTIFIER_BARK_DEVICEKEY="your-device-key"
*/
func TestNotify(t *testing.T) {
flag.Parse()
t.Run("Notify", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("SERVERURL: %v", fServerUrl),
fmt.Sprintf("DEVICEKEY: %v", fDeviceKey),
}, "\n"))
notifier, err := provider.New(&provider.BarkNotifierConfig{
ServerUrl: fServerUrl,
DeviceKey: fDeviceKey,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
res, err := notifier.Notify(context.Background(), mockSubject, mockMessage)
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@ -0,0 +1,63 @@
package dingtalk_test
import (
"context"
"flag"
"fmt"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/dingtalk"
)
const (
mockSubject = "test_subject"
mockMessage = "test_message"
)
var (
fAccessToken string
fSecret string
)
func init() {
argsPrefix := "CERTIMATE_NOTIFIER_DINGTALK_"
flag.StringVar(&fAccessToken, argsPrefix+"ACCESSTOKEN", "", "")
flag.StringVar(&fSecret, argsPrefix+"SECRET", "", "")
}
/*
Shell command to run this test:
go test -v dingtalk_test.go -args \
--CERTIMATE_NOTIFIER_DINGTALK_URL="https://example.com/your-webhook-url"
*/
func TestNotify(t *testing.T) {
flag.Parse()
t.Run("Notify", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("ACCESSTOKEN: %v", fAccessToken),
fmt.Sprintf("SECRET: %v", fSecret),
}, "\n"))
notifier, err := provider.New(&provider.DingTalkNotifierConfig{
AccessToken: fAccessToken,
Secret: fSecret,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
res, err := notifier.Notify(context.Background(), mockSubject, mockMessage)
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@ -1,51 +1,89 @@
package email_test
import (
"os"
"strconv"
"context"
"flag"
"fmt"
"strings"
"testing"
notifierEmail "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/email"
provider "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/email"
)
const (
mockSubject = "test_subject"
mockMessage = "test_message"
)
var (
fSmtpHost string
fSmtpPort int
fSmtpTLS bool
fUsername string
fPassword string
fSenderAddress string
fReceiverAddress string
)
func init() {
argsPrefix := "CERTIMATE_NOTIFIER_EMAIL_"
flag.StringVar(&fSmtpHost, argsPrefix+"SMTPHOST", "", "")
flag.IntVar(&fSmtpPort, argsPrefix+"SMTPPORT", 0, "")
flag.BoolVar(&fSmtpTLS, argsPrefix+"SMTPTLS", false, "")
flag.StringVar(&fUsername, argsPrefix+"USERNAME", "", "")
flag.StringVar(&fPassword, argsPrefix+"PASSWORD", "", "")
flag.StringVar(&fSenderAddress, argsPrefix+"SENDERADDRESS", "", "")
flag.StringVar(&fReceiverAddress, argsPrefix+"RECEIVERADDRESS", "", "")
}
/*
Shell command to run this test:
CERTIMATE_NOTIFIER_EMAIL_SMTPPORT=465 \
CERTIMATE_NOTIFIER_EMAIL_SMTPTLS=true \
CERTIMATE_NOTIFIER_EMAIL_SMTPHOST="smtp.example.com" \
CERTIMATE_NOTIFIER_EMAIL_USERNAME="your-username" \
CERTIMATE_NOTIFIER_EMAIL_PASSWORD="your-password" \
CERTIMATE_NOTIFIER_EMAIL_SENDERADDRESS="sender@example.com" \
CERTIMATE_NOTIFIER_EMAIL_RECEIVERADDRESS="receiver@example.com" \
go test -v -run TestNotify email_test.go
go test -v email_test.go -args \
--CERTIMATE_NOTIFIER_EMAIL_SMTPHOST="smtp.example.com" \
--CERTIMATE_NOTIFIER_EMAIL_SMTPPORT=465 \
--CERTIMATE_NOTIFIER_EMAIL_SMTPTLS=true \
--CERTIMATE_NOTIFIER_EMAIL_USERNAME="your-username" \
--CERTIMATE_NOTIFIER_EMAIL_PASSWORD="your-password" \
--CERTIMATE_NOTIFIER_EMAIL_SENDERADDRESS="sender@example.com" \
--CERTIMATE_NOTIFIER_EMAIL_RECEIVERADDRESS="receiver@example.com"
*/
func TestNotify(t *testing.T) {
smtpPort, err := strconv.ParseInt(os.Getenv("CERTIMATE_NOTIFIER_EMAIL_SMTPPORT"), 10, 32)
if err != nil {
t.Errorf("invalid envvar: %+v", err)
panic(err)
}
flag.Parse()
smtpTLS, err := strconv.ParseBool(os.Getenv("CERTIMATE_NOTIFIER_EMAIL_SMTPTLS"))
if err != nil {
t.Errorf("invalid envvar: %+v", err)
panic(err)
}
t.Run("Notify", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("SMTPHOST: %v", fSmtpHost),
fmt.Sprintf("SMTPPORT: %v", fSmtpPort),
fmt.Sprintf("SMTPTLS: %v", fSmtpTLS),
fmt.Sprintf("USERNAME: %v", fUsername),
fmt.Sprintf("PASSWORD: %v", fPassword),
fmt.Sprintf("SENDERADDRESS: %v", fSenderAddress),
fmt.Sprintf("RECEIVERADDRESS: %v", fReceiverAddress),
}, "\n"))
res, err := notifierEmail.New(&notifierEmail.EmailNotifierConfig{
SmtpHost: os.Getenv("CERTIMATE_NOTIFIER_EMAIL_SMTPHOST"),
SmtpPort: int32(smtpPort),
SmtpTLS: smtpTLS,
Username: os.Getenv("CERTIMATE_NOTIFIER_EMAIL_USERNAME"),
Password: os.Getenv("CERTIMATE_NOTIFIER_EMAIL_PASSWORD"),
SenderAddress: os.Getenv("CERTIMATE_NOTIFIER_EMAIL_SENDERADDRESS"),
ReceiverAddress: os.Getenv("CERTIMATE_NOTIFIER_EMAIL_RECEIVERADDRESS"),
notifier, err := provider.New(&provider.EmailNotifierConfig{
SmtpHost: fSmtpHost,
SmtpPort: int32(fSmtpPort),
SmtpTLS: fSmtpTLS,
Username: fUsername,
Password: fPassword,
SenderAddress: fSenderAddress,
ReceiverAddress: fReceiverAddress,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
res, err := notifier.Notify(context.Background(), mockSubject, mockMessage)
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
if err != nil {
t.Errorf("invalid envvar: %+v", err)
panic(err)
}
t.Logf("notify result: %v", res)
}

View File

@ -0,0 +1,57 @@
package lark_test
import (
"context"
"flag"
"fmt"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/lark"
)
const (
mockSubject = "test_subject"
mockMessage = "test_message"
)
var fWebhookUrl string
func init() {
argsPrefix := "CERTIMATE_NOTIFIER_LARK_"
flag.StringVar(&fWebhookUrl, argsPrefix+"WEBHOOKURL", "", "")
}
/*
Shell command to run this test:
go test -v lark_test.go -args \
--CERTIMATE_NOTIFIER_LARK_WEBHOOKURL="https://example.com/your-webhook-url"
*/
func TestNotify(t *testing.T) {
flag.Parse()
t.Run("Notify", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("WEBHOOKURL: %v", fWebhookUrl),
}, "\n"))
notifier, err := provider.New(&provider.LarkNotifierConfig{
WebhookUrl: fWebhookUrl,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
res, err := notifier.Notify(context.Background(), mockSubject, mockMessage)
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@ -0,0 +1,57 @@
package serverchan_test
import (
"context"
"flag"
"fmt"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/serverchan"
)
const (
mockSubject = "test_subject"
mockMessage = "test_message"
)
var fUrl string
func init() {
argsPrefix := "CERTIMATE_NOTIFIER_SERVERCHAN_"
flag.StringVar(&fUrl, argsPrefix+"URL", "", "")
}
/*
Shell command to run this test:
go test -v serverchan_test.go -args \
--CERTIMATE_NOTIFIER_SERVERCHAN_URL="https://example.com/your-webhook-url" \
*/
func TestNotify(t *testing.T) {
flag.Parse()
t.Run("Notify", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("URL: %v", fUrl),
}, "\n"))
notifier, err := provider.New(&provider.ServerChanNotifierConfig{
Url: fUrl,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
res, err := notifier.Notify(context.Background(), mockSubject, mockMessage)
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@ -0,0 +1,64 @@
package telegram_test
import (
"context"
"flag"
"fmt"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/telegram"
)
const (
mockSubject = "test_subject"
mockMessage = "test_message"
)
var (
fApiToken string
fChartId int64
)
func init() {
argsPrefix := "CERTIMATE_NOTIFIER_TELEGRAM_"
flag.StringVar(&fApiToken, argsPrefix+"APITOKEN", "", "")
flag.Int64Var(&fChartId, argsPrefix+"CHATID", 0, "")
}
/*
Shell command to run this test:
go test -v telegram_test.go -args \
--CERTIMATE_NOTIFIER_TELEGRAM_APITOKEN="your-api-token" \
--CERTIMATE_NOTIFIER_TELEGRAM_CHATID=123456
*/
func TestNotify(t *testing.T) {
flag.Parse()
t.Run("Notify", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("APITOKEN: %v", fApiToken),
fmt.Sprintf("CHATID: %v", fChartId),
}, "\n"))
notifier, err := provider.New(&provider.TelegramNotifierConfig{
ApiToken: fApiToken,
ChatId: fChartId,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
res, err := notifier.Notify(context.Background(), mockSubject, mockMessage)
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@ -0,0 +1,57 @@
package webhook_test
import (
"context"
"flag"
"fmt"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/webhook"
)
const (
mockSubject = "test_subject"
mockMessage = "test_message"
)
var fUrl string
func init() {
argsPrefix := "CERTIMATE_NOTIFIER_WEBHOOK_"
flag.StringVar(&fUrl, argsPrefix+"URL", "", "")
}
/*
Shell command to run this test:
go test -v webhook_test.go -args \
--CERTIMATE_NOTIFIER_WEBHOOK_URL="https://example.com/your-webhook-url"
*/
func TestNotify(t *testing.T) {
flag.Parse()
t.Run("Notify", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("URL: %v", fUrl),
}, "\n"))
notifier, err := provider.New(&provider.WebhookNotifierConfig{
Url: fUrl,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
res, err := notifier.Notify(context.Background(), mockSubject, mockMessage)
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@ -144,11 +144,7 @@ func createSdkClient(accessKeyId, accessKeySecret, region string) (*aliyunCas.Cl
region = "cn-hangzhou" // CAS 服务默认区域:华东一杭州
}
aConfig := &aliyunOpen.Config{
AccessKeyId: tea.String(accessKeyId),
AccessKeySecret: tea.String(accessKeySecret),
}
// 接入点一览 https://help.aliyun.com/zh/ssl-certificate/developer-reference/endpoints
var endpoint string
switch region {
case "cn-hangzhou":
@ -156,9 +152,14 @@ func createSdkClient(accessKeyId, accessKeySecret, region string) (*aliyunCas.Cl
default:
endpoint = fmt.Sprintf("cas.%s.aliyuncs.com", region)
}
aConfig.Endpoint = tea.String(endpoint)
client, err := aliyunCas.NewClient(aConfig)
config := &aliyunOpen.Config{
Endpoint: tea.String(endpoint),
AccessKeyId: tea.String(accessKeyId),
AccessKeySecret: tea.String(accessKeySecret),
}
client, err := aliyunCas.NewClient(config)
if err != nil {
return nil, err
}

View File

@ -121,11 +121,7 @@ func createSdkClient(accessKeyId, accessKeySecret, region string) (*aliyunSlb.Cl
region = "cn-hangzhou" // SLB 服务默认区域:华东一杭州
}
aConfig := &aliyunOpen.Config{
AccessKeyId: tea.String(accessKeyId),
AccessKeySecret: tea.String(accessKeySecret),
}
// 接入点一览 https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-endpoint
var endpoint string
switch region {
case
@ -137,9 +133,14 @@ func createSdkClient(accessKeyId, accessKeySecret, region string) (*aliyunSlb.Cl
default:
endpoint = fmt.Sprintf("slb.%s.aliyuncs.com", region)
}
aConfig.Endpoint = tea.String(endpoint)
client, err := aliyunSlb.NewClient(aConfig)
config := &aliyunOpen.Config{
Endpoint: tea.String(endpoint),
AccessKeyId: tea.String(accessKeyId),
AccessKeySecret: tea.String(accessKeySecret),
}
client, err := aliyunSlb.NewClient(config)
if err != nil {
return nil, err
}

View File

@ -2,6 +2,7 @@ package bytepluscdn
import (
"context"
"crypto/sha1"
"crypto/sha256"
"encoding/hex"
"errors"
@ -9,9 +10,11 @@ import (
"strings"
"time"
"github.com/byteplus-sdk/byteplus-sdk-golang/service/cdn"
bpCdn "github.com/byteplus-sdk/byteplus-sdk-golang/service/cdn"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
"github.com/usual2970/certimate/internal/pkg/utils/cast"
"github.com/usual2970/certimate/internal/pkg/utils/x509"
)
@ -22,7 +25,7 @@ type ByteplusCDNUploaderConfig struct {
type ByteplusCDNUploader struct {
config *ByteplusCDNUploaderConfig
sdkClient *cdn.CDN
sdkClient *bpCdn.CDN
}
var _ uploader.Uploader = (*ByteplusCDNUploader)(nil)
@ -32,14 +35,13 @@ func New(config *ByteplusCDNUploaderConfig) (*ByteplusCDNUploader, error) {
return nil, errors.New("config is nil")
}
instance := cdn.NewInstance()
client := instance.Client
client.SetAccessKey(config.AccessKey)
client.SetSecretKey(config.SecretKey)
client := bpCdn.NewInstance()
client.Client.SetAccessKey(config.AccessKey)
client.Client.SetSecretKey(config.SecretKey)
return &ByteplusCDNUploader{
config: config,
sdkClient: instance,
sdkClient: client,
}, nil
}
@ -49,17 +51,17 @@ func (u *ByteplusCDNUploader) Upload(ctx context.Context, certPem string, privke
if err != nil {
return nil, err
}
// 查询证书列表,避免重复上传
// REF: https://docs.byteplus.com/en/docs/byteplus-cdn/reference-listcertinfo
pageNum := int64(1)
pageSize := int64(100)
certSource := "cert_center"
listCertInfoReq := &cdn.ListCertInfoRequest{
PageNum: &pageNum,
PageSize: &pageSize,
Source: &certSource,
listCertInfoPageNum := int64(1)
listCertInfoPageSize := int64(100)
listCertInfoTotal := 0
listCertInfoReq := &bpCdn.ListCertInfoRequest{
PageNum: cast.Int64Ptr(listCertInfoPageNum),
PageSize: cast.Int64Ptr(listCertInfoPageSize),
Source: cast.StringPtr("cert_center"),
}
searchTotal := 0
for {
listCertInfoResp, err := u.sdkClient.ListCertInfo(listCertInfoReq)
if err != nil {
@ -68,8 +70,10 @@ func (u *ByteplusCDNUploader) Upload(ctx context.Context, certPem string, privke
if listCertInfoResp.Result.CertInfo != nil {
for _, certDetail := range listCertInfoResp.Result.CertInfo {
hash := sha256.Sum256(certX509.Raw)
isSameCert := strings.EqualFold(hex.EncodeToString(hash[:]), certDetail.CertFingerprint.Sha256)
fingerprintSha1 := sha1.Sum(certX509.Raw)
fingerprintSha256 := sha256.Sum256(certX509.Raw)
isSameCert := strings.EqualFold(hex.EncodeToString(fingerprintSha1[:]), certDetail.CertFingerprint.Sha1) &&
strings.EqualFold(hex.EncodeToString(fingerprintSha256[:]), certDetail.CertFingerprint.Sha256)
// 如果已存在相同证书,直接返回已有的证书信息
if isSameCert {
return &uploader.UploadResult{
@ -80,23 +84,26 @@ func (u *ByteplusCDNUploader) Upload(ctx context.Context, certPem string, privke
}
}
searchTotal += len(listCertInfoResp.Result.CertInfo)
if int(listCertInfoResp.Result.Total) > searchTotal {
pageNum++
} else {
listCertInfoLen := len(listCertInfoResp.Result.CertInfo)
if listCertInfoLen < int(listCertInfoPageSize) || int(listCertInfoResp.Result.Total) <= listCertInfoTotal+listCertInfoLen {
break
} else {
listCertInfoPageNum++
listCertInfoTotal += listCertInfoLen
}
}
// 生成新证书名(需符合 BytePlus 命名规则)
var certId, certName string
certName = fmt.Sprintf("certimate-%d", time.Now().UnixMilli())
// 上传新证书
// REF: https://docs.byteplus.com/en/docs/byteplus-cdn/reference-addcertificate
addCertificateReq := &cdn.AddCertificateRequest{
addCertificateReq := &bpCdn.AddCertificateRequest{
Certificate: certPem,
PrivateKey: privkeyPem,
Source: &certSource,
Desc: &certName,
Source: cast.StringPtr("cert_center"),
Desc: cast.StringPtr(certName),
}
addCertificateResp, err := u.sdkClient.AddCertificate(addCertificateReq)
if err != nil {

View File

@ -56,7 +56,7 @@ func New(config *HuaweiCloudELBUploaderConfig) (*HuaweiCloudELBUploader, error)
func (u *HuaweiCloudELBUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) {
// 解析证书内容
newCert, err := x509.ParseCertificateFromPEM(certPem)
certX509, err := x509.ParseCertificateFromPEM(certPem)
if err != nil {
return nil, err
}
@ -83,12 +83,12 @@ func (u *HuaweiCloudELBUploader) Upload(ctx context.Context, certPem string, pri
if certDetail.Certificate == certPem {
isSameCert = true
} else {
cert, err := x509.ParseCertificateFromPEM(certDetail.Certificate)
oldCertX509, err := x509.ParseCertificateFromPEM(certDetail.Certificate)
if err != nil {
continue
}
isSameCert = x509.EqualCertificate(cert, newCert)
isSameCert = x509.EqualCertificate(certX509, oldCertX509)
}
// 如果已存在相同证书,直接返回已有的证书信息
@ -205,9 +205,6 @@ func getSdkProjectId(accessKeyId, secretAccessKey, region string) (string, error
}
client := hcIam.NewIamClient(hcClient)
if err != nil {
return "", err
}
request := &hcIamModel.KeystoneListProjectsRequest{
Name: &region,

View File

@ -92,12 +92,12 @@ func (u *HuaweiCloudSCMUploader) Upload(ctx context.Context, certPem string, pri
if *exportCertificateResp.Certificate == certPem {
isSameCert = true
} else {
cert, err := x509.ParseCertificateFromPEM(*exportCertificateResp.Certificate)
oldCertX509, err := x509.ParseCertificateFromPEM(*exportCertificateResp.Certificate)
if err != nil {
continue
}
isSameCert = x509.EqualCertificate(certX509, cert)
isSameCert = x509.EqualCertificate(certX509, oldCertX509)
}
// 如果已存在相同证书,直接返回已有的证书信息

View File

@ -2,6 +2,7 @@ package volcenginecdn
import (
"context"
"crypto/sha1"
"crypto/sha256"
"encoding/hex"
"errors"
@ -10,56 +11,57 @@ import (
"time"
xerrors "github.com/pkg/errors"
veCdn "github.com/volcengine/volc-sdk-golang/service/cdn"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
"github.com/usual2970/certimate/internal/pkg/utils/cast"
"github.com/usual2970/certimate/internal/pkg/utils/x509"
"github.com/volcengine/volc-sdk-golang/service/cdn"
)
type VolcengineCDNUploaderConfig struct {
type VolcEngineCDNUploaderConfig struct {
AccessKeyId string `json:"accessKeyId"`
AccessKeySecret string `json:"accessKeySecret"`
}
type VolcengineCDNUploader struct {
config *VolcengineCDNUploaderConfig
sdkClient *cdn.CDN
type VolcEngineCDNUploader struct {
config *VolcEngineCDNUploaderConfig
sdkClient *veCdn.CDN
}
var _ uploader.Uploader = (*VolcengineCDNUploader)(nil)
var _ uploader.Uploader = (*VolcEngineCDNUploader)(nil)
func New(config *VolcengineCDNUploaderConfig) (*VolcengineCDNUploader, error) {
func New(config *VolcEngineCDNUploaderConfig) (*VolcEngineCDNUploader, error) {
if config == nil {
return nil, errors.New("config is nil")
}
instance := cdn.NewInstance()
client := instance.Client
client.SetAccessKey(config.AccessKeyId)
client.SetSecretKey(config.AccessKeySecret)
client := veCdn.NewInstance()
client.Client.SetAccessKey(config.AccessKeyId)
client.Client.SetSecretKey(config.AccessKeySecret)
return &VolcengineCDNUploader{
return &VolcEngineCDNUploader{
config: config,
sdkClient: instance,
sdkClient: client,
}, nil
}
func (u *VolcengineCDNUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) {
func (u *VolcEngineCDNUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) {
// 解析证书内容
certX509, err := x509.ParseCertificateFromPEM(certPem)
if err != nil {
return nil, err
}
// 查询证书列表,避免重复上传
// REF: https://www.volcengine.com/docs/6454/125709
pageNum := int64(1)
pageSize := int64(100)
certSource := "volc_cert_center"
listCertInfoReq := &cdn.ListCertInfoRequest{
PageNum: &pageNum,
PageSize: &pageSize,
Source: certSource,
listCertInfoPageNum := int64(1)
listCertInfoPageSize := int64(100)
listCertInfoTotal := 0
listCertInfoReq := &veCdn.ListCertInfoRequest{
PageNum: cast.Int64Ptr(listCertInfoPageNum),
PageSize: cast.Int64Ptr(listCertInfoPageSize),
Source: "volc_cert_center",
}
searchTotal := 0
for {
listCertInfoResp, err := u.sdkClient.ListCertInfo(listCertInfoReq)
if err != nil {
@ -68,8 +70,10 @@ func (u *VolcengineCDNUploader) Upload(ctx context.Context, certPem string, priv
if listCertInfoResp.Result.CertInfo != nil {
for _, certDetail := range listCertInfoResp.Result.CertInfo {
hash := sha256.Sum256(certX509.Raw)
isSameCert := strings.EqualFold(hex.EncodeToString(hash[:]), certDetail.CertFingerprint.Sha256)
fingerprintSha1 := sha1.Sum(certX509.Raw)
fingerprintSha256 := sha256.Sum256(certX509.Raw)
isSameCert := strings.EqualFold(hex.EncodeToString(fingerprintSha1[:]), certDetail.CertFingerprint.Sha1) &&
strings.EqualFold(hex.EncodeToString(fingerprintSha256[:]), certDetail.CertFingerprint.Sha256)
// 如果已存在相同证书,直接返回已有的证书信息
if isSameCert {
return &uploader.UploadResult{
@ -80,24 +84,26 @@ func (u *VolcengineCDNUploader) Upload(ctx context.Context, certPem string, priv
}
}
searchTotal += len(listCertInfoResp.Result.CertInfo)
if int(listCertInfoResp.Result.Total) > searchTotal {
pageNum++
} else {
listCertInfoLen := len(listCertInfoResp.Result.CertInfo)
if listCertInfoLen < int(listCertInfoPageSize) || int(listCertInfoResp.Result.Total) <= listCertInfoTotal+listCertInfoLen {
break
} else {
listCertInfoPageNum++
listCertInfoTotal += listCertInfoLen
}
}
// 生成新证书名(需符合火山引擎命名规则)
var certId, certName string
certName = fmt.Sprintf("certimate-%d", time.Now().UnixMilli())
// 上传新证书
// REF: https://www.volcengine.com/docs/6454/1245763
addCertificateReq := &cdn.AddCertificateRequest{
addCertificateReq := &veCdn.AddCertificateRequest{
Certificate: certPem,
PrivateKey: privkeyPem,
Source: &certSource,
Desc: &certName,
Source: cast.StringPtr("volc_cert_center"),
Desc: cast.StringPtr(certName),
}
addCertificateResp, err := u.sdkClient.AddCertificate(addCertificateReq)
if err != nil {

View File

@ -8,77 +8,79 @@ import (
"time"
xerrors "github.com/pkg/errors"
veLive "github.com/volcengine/volc-sdk-golang/service/live/v20230101"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
"github.com/usual2970/certimate/internal/pkg/utils/cast"
"github.com/usual2970/certimate/internal/pkg/utils/x509"
live "github.com/volcengine/volc-sdk-golang/service/live/v20230101"
)
type VolcengineLiveUploaderConfig struct {
type VolcEngineLiveUploaderConfig struct {
AccessKeyId string `json:"accessKeyId"`
AccessKeySecret string `json:"accessKeySecret"`
}
type VolcengineLiveUploader struct {
config *VolcengineLiveUploaderConfig
sdkClient *live.Live
type VolcEngineLiveUploader struct {
config *VolcEngineLiveUploaderConfig
sdkClient *veLive.Live
}
var _ uploader.Uploader = (*VolcengineLiveUploader)(nil)
var _ uploader.Uploader = (*VolcEngineLiveUploader)(nil)
func New(config *VolcengineLiveUploaderConfig) (*VolcengineLiveUploader, error) {
func New(config *VolcEngineLiveUploaderConfig) (*VolcEngineLiveUploader, error) {
if config == nil {
return nil, errors.New("config is nil")
}
client := live.NewInstance()
client := veLive.NewInstance()
client.SetAccessKey(config.AccessKeyId)
client.SetSecretKey(config.AccessKeySecret)
return &VolcengineLiveUploader{
return &VolcEngineLiveUploader{
config: config,
sdkClient: client,
}, nil
}
func (u *VolcengineLiveUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) {
func (u *VolcEngineLiveUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) {
// 解析证书内容
certX509, err := x509.ParseCertificateFromPEM(certPem)
if err != nil {
return nil, err
}
// 查询证书列表,避免重复上传
// REF: https://www.volcengine.com/docs/6469/1186278#%E6%9F%A5%E8%AF%A2%E8%AF%81%E4%B9%A6%E5%88%97%E8%A1%A8
listCertReq := &live.ListCertV2Body{}
listCertReq := &veLive.ListCertV2Body{}
listCertResp, err := u.sdkClient.ListCertV2(ctx, listCertReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'live.ListCertV2'")
}
if listCertResp.Result.CertList != nil {
for _, certDetail := range listCertResp.Result.CertList {
describeCertDetailSecretReq := &live.DescribeCertDetailSecretV2Body{
ChainID: cast.StringPtr(certDetail.ChainID),
}
// 查询证书详细信息
// REF: https://www.volcengine.com/docs/6469/1186278#%E6%9F%A5%E7%9C%8B%E8%AF%81%E4%B9%A6%E8%AF%A6%E6%83%85
describeCertDetailSecretResp, detailErr := u.sdkClient.DescribeCertDetailSecretV2(ctx, describeCertDetailSecretReq)
if detailErr != nil {
describeCertDetailSecretReq := &veLive.DescribeCertDetailSecretV2Body{
ChainID: cast.StringPtr(certDetail.ChainID),
}
describeCertDetailSecretResp, err := u.sdkClient.DescribeCertDetailSecretV2(ctx, describeCertDetailSecretReq)
if err != nil {
continue
}
var isSameCert bool
certificate := strings.Join(describeCertDetailSecretResp.Result.SSL.Chain, "\n\n")
if certificate == certPem {
isSameCert = true
} else {
cert, err := x509.ParseCertificateFromPEM(certificate)
oldCertX509, err := x509.ParseCertificateFromPEM(certificate)
if err != nil {
continue
}
isSameCert = x509.EqualCertificate(cert, certX509)
isSameCert = x509.EqualCertificate(certX509, oldCertX509)
}
// 如果已存在相同证书,直接返回已有的证书信息
if isSameCert {
return &uploader.UploadResult{
@ -92,13 +94,14 @@ func (u *VolcengineLiveUploader) Upload(ctx context.Context, certPem string, pri
// 生成新证书名(需符合火山引擎命名规则)
var certId, certName string
certName = fmt.Sprintf("certimate-%d", time.Now().UnixMilli())
// 上传新证书
// REF: https://www.volcengine.com/docs/6469/1186278#%E6%B7%BB%E5%8A%A0%E8%AF%81%E4%B9%A6
createCertReq := &live.CreateCertBody{
createCertReq := &veLive.CreateCertBody{
CertName: &certName,
UseWay: "https",
ProjectName: cast.StringPtr("default"),
Rsa: live.CreateCertBodyRsa{
Rsa: veLive.CreateCertBodyRsa{
Prikey: privkeyPem,
Pubkey: certPem,
},

View File

@ -22,7 +22,7 @@ func GetValueAsString(dict map[string]any, key string) string {
// - defaultValue: 默认值。
//
// 出参:
// - 字典中键对应的值。如果指定键不存在或者值的类型不是字符串,则返回默认值。
// - 字典中键对应的值。如果指定键不存在、值的类型不是字符串或者值为零值,则返回默认值。
func GetValueOrDefaultAsString(dict map[string]any, key string, defaultValue string) string {
if dict == nil {
return defaultValue
@ -30,7 +30,9 @@ func GetValueOrDefaultAsString(dict map[string]any, key string, defaultValue str
if value, ok := dict[key]; ok {
if result, ok := value.(string); ok {
return result
if result != "" {
return result
}
}
}
@ -57,7 +59,7 @@ func GetValueAsInt32(dict map[string]any, key string) int32 {
// - defaultValue: 默认值。
//
// 出参:
// - 字典中键对应的值。如果指定键不存在或者值的类型不是 32 位整数,则返回默认值。
// - 字典中键对应的值。如果指定键不存在、值的类型不是 32 位整数或者值为零值,则返回默认值。
func GetValueOrDefaultAsInt32(dict map[string]any, key string, defaultValue int32) int32 {
if dict == nil {
return defaultValue
@ -65,13 +67,17 @@ func GetValueOrDefaultAsInt32(dict map[string]any, key string, defaultValue int3
if value, ok := dict[key]; ok {
if result, ok := value.(int32); ok {
return result
if result != 0 {
return result
}
}
// 兼容字符串类型的值
if str, ok := value.(string); ok {
if result, err := strconv.ParseInt(str, 10, 32); err == nil {
return int32(result)
if result != 0 {
return int32(result)
}
}
}
}
@ -99,7 +105,7 @@ func GetValueAsInt64(dict map[string]any, key string) int64 {
// - defaultValue: 默认值。
//
// 出参:
// - 字典中键对应的值。如果指定键不存在或者值的类型不是 64 位整数,则返回默认值。
// - 字典中键对应的值。如果指定键不存在、值的类型不是 64 位整数或者值为零值,则返回默认值。
func GetValueOrDefaultAsInt64(dict map[string]any, key string, defaultValue int64) int64 {
if dict == nil {
return defaultValue
@ -107,13 +113,17 @@ func GetValueOrDefaultAsInt64(dict map[string]any, key string, defaultValue int6
if value, ok := dict[key]; ok {
if result, ok := value.(int64); ok {
return result
if result != 0 {
return result
}
}
// 兼容字符串类型的值
if str, ok := value.(string); ok {
if result, err := strconv.ParseInt(str, 10, 64); err == nil {
return result
if result != 0 {
return result
}
}
}
}

View File

@ -0,0 +1,87 @@
package x509
import (
"bytes"
"encoding/pem"
"errors"
"time"
"github.com/pavlo-v-chernykh/keystore-go/v4"
"software.sslmate.com/src/go-pkcs12"
)
// 将 PEM 编码的证书字符串转换为 PFX 格式。
//
// 入参:
// - certPem: 证书 PEM 内容。
// - privkeyPem: 私钥 PEM 内容。
// - pfxPassword: PFX 导出密码。
//
// 出参:
// - data: PFX 格式的证书数据。
// - err: 错误。
func TransformCertificateFromPEMToPFX(certPem string, privkeyPem string, pfxPassword string) ([]byte, error) {
cert, err := ParseCertificateFromPEM(certPem)
if err != nil {
return nil, err
}
privkey, err := ParsePKCS1PrivateKeyFromPEM(privkeyPem)
if err != nil {
return nil, err
}
pfxData, err := pkcs12.LegacyRC2.Encode(privkey, cert, nil, pfxPassword)
if err != nil {
return nil, err
}
return pfxData, nil
}
// 将 PEM 编码的证书字符串转换为 JKS 格式。
//
// 入参:
// - certPem: 证书 PEM 内容。
// - privkeyPem: 私钥 PEM 内容。
// - jksAlias: JKS 别名。
// - jksKeypass: JKS 密钥密码。
// - jksStorepass: JKS 存储密码。
//
// 出参:
// - data: JKS 格式的证书数据。
// - err: 错误。
func TransformCertificateFromPEMToJKS(certPem string, privkeyPem string, jksAlias string, jksKeypass string, jksStorepass string) ([]byte, error) {
certBlock, _ := pem.Decode([]byte(certPem))
if certBlock == nil {
return nil, errors.New("failed to decode certificate PEM")
}
privkeyBlock, _ := pem.Decode([]byte(privkeyPem))
if privkeyBlock == nil {
return nil, errors.New("failed to decode private key PEM")
}
ks := keystore.New()
entry := keystore.PrivateKeyEntry{
CreationTime: time.Now(),
PrivateKey: privkeyBlock.Bytes,
CertificateChain: []keystore.Certificate{
{
Type: "X509",
Content: certBlock.Bytes,
},
},
}
if err := ks.SetPrivateKeyEntry(jksAlias, entry, []byte(jksKeypass)); err != nil {
return nil, err
}
var buf bytes.Buffer
if err := ks.Store(&buf, []byte(jksStorepass)); err != nil {
return nil, err
}
return buf.Bytes(), nil
}