This commit is contained in:
yoan
2024-10-13 08:15:21 +08:00
parent 19f5348802
commit 1928a47961
37 changed files with 1854 additions and 734 deletions

View File

@@ -3,6 +3,7 @@ package applicant
import (
"certimate/internal/domain"
"encoding/json"
"fmt"
"os"
"github.com/go-acme/lego/v4/providers/dns/alidns"
@@ -25,6 +26,7 @@ func (a *aliyun) Apply() (*Certificate, error) {
os.Setenv("ALICLOUD_ACCESS_KEY", access.AccessKeyId)
os.Setenv("ALICLOUD_SECRET_KEY", access.AccessKeySecret)
os.Setenv("ALICLOUD_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", a.option.Timeout))
dnsProvider, err := alidns.NewDNSProvider()
if err != nil {
return nil, err

View File

@@ -1,6 +1,7 @@
package applicant
import (
"certimate/internal/domain"
"certimate/internal/utils/app"
"crypto"
"crypto/ecdsa"
@@ -46,6 +47,8 @@ var sslProviderUrls = map[string]string{
const defaultEmail = "536464346@qq.com"
const defaultTimeout = 60
type Certificate struct {
CertUrl string `json:"certUrl"`
CertStableUrl string `json:"certStableUrl"`
@@ -60,6 +63,7 @@ type ApplyOption struct {
Domain string `json:"domain"`
Access string `json:"access"`
Nameservers string `json:"nameservers"`
Timeout int64 `json:"timeout"`
}
type MyUser struct {
@@ -83,8 +87,22 @@ type Applicant interface {
}
func Get(record *models.Record) (Applicant, error) {
access := record.ExpandedOne("access")
email := record.GetString("email")
if record.GetString("applyConfig") == "" {
return nil, errors.New("apply config is empty")
}
applyConfig := &domain.ApplyConfig{}
record.UnmarshalJSONField("applyConfig", applyConfig)
access, err := app.GetApp().Dao().FindRecordById("access", applyConfig.Access)
if err != nil {
return nil, fmt.Errorf("access record not found: %w", err)
}
email := applyConfig.Email
if email == "" {
email = defaultEmail
}
@@ -92,7 +110,8 @@ func Get(record *models.Record) (Applicant, error) {
Email: email,
Domain: record.GetString("domain"),
Access: access.GetString("config"),
Nameservers: record.GetString("nameservers"),
Nameservers: applyConfig.Nameservers,
Timeout: applyConfig.Timeout,
}
switch access.GetString("configType") {
case configTypeAliyun:

View File

@@ -3,6 +3,7 @@ package applicant
import (
"certimate/internal/domain"
"encoding/json"
"fmt"
"os"
cf "github.com/go-acme/lego/v4/providers/dns/cloudflare"
@@ -23,6 +24,7 @@ func (c *cloudflare) Apply() (*Certificate, error) {
json.Unmarshal([]byte(c.option.Access), access)
os.Setenv("CLOUDFLARE_DNS_API_TOKEN", access.DnsApiToken)
os.Setenv("CLOUDFLARE_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", c.option.Timeout))
provider, err := cf.NewDNSProvider()
if err != nil {

View File

@@ -3,6 +3,7 @@ package applicant
import (
"certimate/internal/domain"
"encoding/json"
"fmt"
"os"
godaddyProvider "github.com/go-acme/lego/v4/providers/dns/godaddy"
@@ -25,6 +26,7 @@ func (a *godaddy) Apply() (*Certificate, error) {
os.Setenv("GODADDY_API_KEY", access.ApiKey)
os.Setenv("GODADDY_API_SECRET", access.ApiSecret)
os.Setenv("GODADDY_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", a.option.Timeout))
dnsProvider, err := godaddyProvider.NewDNSProvider()
if err != nil {

View File

@@ -3,6 +3,7 @@ package applicant
import (
"certimate/internal/domain"
"encoding/json"
"fmt"
"os"
huaweicloudProvider "github.com/go-acme/lego/v4/providers/dns/huaweicloud"
@@ -26,6 +27,8 @@ func (t *huaweicloud) Apply() (*Certificate, error) {
os.Setenv("HUAWEICLOUD_REGION", access.Region) // 华为云的 SDK 要求必须传一个区域,实际上 DNS-01 流程里用不到,但不传会报错
os.Setenv("HUAWEICLOUD_ACCESS_KEY_ID", access.AccessKeyId)
os.Setenv("HUAWEICLOUD_SECRET_ACCESS_KEY", access.SecretAccessKey)
os.Setenv("HUAWEICLOUD_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", t.option.Timeout))
dnsProvider, err := huaweicloudProvider.NewDNSProvider()
if err != nil {
return nil, err

View File

@@ -3,6 +3,7 @@ package applicant
import (
"certimate/internal/domain"
"encoding/json"
"fmt"
"os"
namesiloProvider "github.com/go-acme/lego/v4/providers/dns/namesilo"
@@ -24,6 +25,7 @@ func (a *namesilo) Apply() (*Certificate, error) {
json.Unmarshal([]byte(a.option.Access), access)
os.Setenv("NAMESILO_API_KEY", access.ApiKey)
os.Setenv("NAMESILO_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", a.option.Timeout))
dnsProvider, err := namesiloProvider.NewDNSProvider()
if err != nil {

View File

@@ -3,6 +3,7 @@ package applicant
import (
"certimate/internal/domain"
"encoding/json"
"fmt"
"os"
"github.com/go-acme/lego/v4/providers/dns/tencentcloud"
@@ -25,6 +26,8 @@ func (t *tencent) Apply() (*Certificate, error) {
os.Setenv("TENCENTCLOUD_SECRET_ID", access.SecretId)
os.Setenv("TENCENTCLOUD_SECRET_KEY", access.SecretKey)
os.Setenv("TENCENTCLOUD_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", t.option.Timeout))
dnsProvider, err := tencentcloud.NewDNSProvider()
if err != nil {
return nil, err

View File

@@ -190,7 +190,7 @@ func (a *aliyun) resource() (*cas20200407.ListCloudResourcesResponseBodyData, er
listCloudResourcesRequest := &cas20200407.ListCloudResourcesRequest{
CloudProduct: tea.String(a.option.Product),
Keyword: tea.String(a.option.Domain),
Keyword: tea.String(getDeployString(a.option.DeployConfig, "domain")),
}
resp, err := a.client.ListCloudResources(listCloudResourcesRequest)

View File

@@ -2,6 +2,7 @@ package deployer
import (
"certimate/internal/domain"
"certimate/internal/utils/rand"
"context"
"encoding/json"
"fmt"
@@ -46,9 +47,9 @@ func (a *AliyunCdn) GetInfo() []string {
func (a *AliyunCdn) Deploy(ctx context.Context) error {
certName := fmt.Sprintf("%s-%s", a.option.Domain, a.option.DomainId)
certName := fmt.Sprintf("%s-%s-%s", a.option.Domain, a.option.DomainId, rand.RandStr(6))
setCdnDomainSSLCertificateRequest := &cdn20180510.SetCdnDomainSSLCertificateRequest{
DomainName: tea.String(a.option.Domain),
DomainName: tea.String(getDeployString(a.option.DeployConfig, "domain")),
CertName: tea.String(certName),
CertType: tea.String("upload"),
SSLProtocol: tea.String("on"),

View File

@@ -7,6 +7,7 @@ package deployer
import (
"certimate/internal/domain"
"certimate/internal/utils/rand"
"context"
"encoding/json"
"fmt"
@@ -51,9 +52,9 @@ func (a *AliyunEsa) GetInfo() []string {
func (a *AliyunEsa) Deploy(ctx context.Context) error {
certName := fmt.Sprintf("%s-%s", a.option.Domain, a.option.DomainId)
certName := fmt.Sprintf("%s-%s-%s", a.option.Domain, a.option.DomainId, rand.RandStr(6))
setDcdnDomainSSLCertificateRequest := &dcdn20180115.SetDcdnDomainSSLCertificateRequest{
DomainName: tea.String(a.option.Domain),
DomainName: tea.String(getDeployString(a.option.DeployConfig, "domain")),
CertName: tea.String(certName),
CertType: tea.String("upload"),
SSLProtocol: tea.String("on"),

View File

@@ -2,8 +2,8 @@ package deployer
import (
"certimate/internal/applicant"
"certimate/internal/domain"
"certimate/internal/utils/app"
"certimate/internal/utils/variables"
"context"
"encoding/json"
"errors"
@@ -30,6 +30,7 @@ type DeployerOption struct {
Product string `json:"product"`
Access string `json:"access"`
AceessRecord *models.Record `json:"-"`
DeployConfig domain.DeployConfig `json:"deployConfig"`
Certificate applicant.Certificate `json:"certificate"`
Variables map[string]string `json:"variables"`
}
@@ -42,52 +43,29 @@ type Deployer interface {
func Gets(record *models.Record, cert *applicant.Certificate) ([]Deployer, error) {
rs := make([]Deployer, 0)
if record.GetString("targetAccess") != "" {
singleDeployer, err := Get(record, cert)
if err != nil {
return nil, err
}
rs = append(rs, singleDeployer)
if record.GetString("deployConfig") == "" {
return rs, nil
}
if record.GetString("group") != "" {
group := record.ExpandedOne("group")
if errs := app.GetApp().Dao().ExpandRecord(group, []string{"access"}, nil); len(errs) > 0 {
errList := make([]error, 0)
for name, err := range errs {
errList = append(errList, fmt.Errorf("展开记录失败,%s: %w", name, err))
}
err := errors.Join(errList...)
return nil, err
}
records := group.ExpandedAll("access")
deployers, err := getByGroup(record, cert, records...)
if err != nil {
return nil, err
}
rs = append(rs, deployers...)
deployConfigs := make([]domain.DeployConfig, 0)
err := record.UnmarshalJSONField("deployConfig", &deployConfigs)
if err != nil {
return nil, fmt.Errorf("解析部署配置失败: %w", err)
}
return rs, nil
if len(deployConfigs) == 0 {
return rs, nil
}
}
for _, deployConfig := range deployConfigs {
func getByGroup(record *models.Record, cert *applicant.Certificate, accesses ...*models.Record) ([]Deployer, error) {
deployer, err := getWithDeployConfig(record, cert, deployConfig)
rs := make([]Deployer, 0)
for _, access := range accesses {
deployer, err := getWithAccess(record, cert, access)
if err != nil {
return nil, err
}
rs = append(rs, deployer)
}
@@ -95,15 +73,21 @@ func getByGroup(record *models.Record, cert *applicant.Certificate, accesses ...
}
func getWithAccess(record *models.Record, cert *applicant.Certificate, access *models.Record) (Deployer, error) {
func getWithDeployConfig(record *models.Record, cert *applicant.Certificate, deployConfig domain.DeployConfig) (Deployer, error) {
access, err := app.GetApp().Dao().FindRecordById("access", deployConfig.Access)
if err != nil {
return nil, fmt.Errorf("access record not found: %w", err)
}
option := &DeployerOption{
DomainId: record.Id,
Domain: record.GetString("domain"),
Product: getProduct(record),
Product: getProduct(deployConfig.Type),
Access: access.GetString("config"),
AceessRecord: access,
Variables: variables.Parse2Map(record.GetString("variables")),
DeployConfig: deployConfig,
}
if cert != nil {
option.Certificate = *cert
@@ -114,7 +98,7 @@ func getWithAccess(record *models.Record, cert *applicant.Certificate, access *m
}
}
switch record.GetString("targetType") {
switch deployConfig.Type {
case targetAliyunOss:
return NewAliyun(option)
case targetAliyunCdn:
@@ -136,16 +120,8 @@ func getWithAccess(record *models.Record, cert *applicant.Certificate, access *m
return nil, errors.New("not implemented")
}
func Get(record *models.Record, cert *applicant.Certificate) (Deployer, error) {
access := record.ExpandedOne("targetAccess")
return getWithAccess(record, cert, access)
}
func getProduct(record *models.Record) string {
targetType := record.GetString("targetType")
rs := strings.Split(targetType, "-")
func getProduct(t string) string {
rs := strings.Split(t, "-")
if len(rs) < 2 {
return ""
}
@@ -159,3 +135,39 @@ func toStr(tag string, data any) string {
byts, _ := json.Marshal(data)
return tag + "" + string(byts)
}
func getDeployString(conf domain.DeployConfig, key string) string {
if _, ok := conf.Config[key]; !ok {
return ""
}
val, ok := conf.Config[key].(string)
if !ok {
return ""
}
return val
}
func getDeployVariables(conf domain.DeployConfig) map[string]string {
rs := make(map[string]string)
data, ok := conf.Config["variables"]
if !ok {
return rs
}
bts, _ := json.Marshal(data)
kvData := make([]domain.KV, 0)
if err := json.Unmarshal(bts, &kvData); err != nil {
return rs
}
for _, kv := range kvData {
rs[kv.Key] = kv.Value
}
return rs
}

View File

@@ -11,9 +11,6 @@ import (
)
type localAccess struct {
Command string `json:"command"`
CertPath string `json:"certPath"`
KeyPath string `json:"keyPath"`
}
type local struct {
@@ -41,18 +38,27 @@ func (l *local) Deploy(ctx context.Context) error {
if err := json.Unmarshal([]byte(l.option.Access), access); err != nil {
return err
}
preCommand := getDeployString(l.option.DeployConfig, "preCommand")
if preCommand != "" {
if err := execCmd(preCommand); err != nil {
return fmt.Errorf("执行前置命令失败: %w", err)
}
}
// 复制文件
if err := copyFile(l.option.Certificate.Certificate, access.CertPath); err != nil {
if err := copyFile(l.option.Certificate.Certificate, getDeployString(l.option.DeployConfig, "certPath")); err != nil {
return fmt.Errorf("复制证书失败: %w", err)
}
if err := copyFile(l.option.Certificate.PrivateKey, access.KeyPath); err != nil {
if err := copyFile(l.option.Certificate.PrivateKey, getDeployString(l.option.DeployConfig, "keyPath")); err != nil {
return fmt.Errorf("复制私钥失败: %w", err)
}
// 执行命令
if err := execCmd(access.Command); err != nil {
if err := execCmd(getDeployString(l.option.DeployConfig, "command")); err != nil {
return fmt.Errorf("执行命令失败: %w", err)
}

View File

@@ -78,7 +78,7 @@ func (q *qiuniu) Deploy(ctx context.Context) error {
}
func (q *qiuniu) enableHttps(certId string) error {
path := fmt.Sprintf("/domain/%s/sslize", q.option.Domain)
path := fmt.Sprintf("/domain/%s/sslize", getDeployString(q.option.DeployConfig, "domain"))
body := &modifyDomainCertReq{
CertID: certId,
@@ -104,7 +104,7 @@ type domainInfo struct {
}
func (q *qiuniu) getDomainInfo() (*domainInfo, error) {
path := fmt.Sprintf("/domain/%s", q.option.Domain)
path := fmt.Sprintf("/domain/%s", getDeployString(q.option.DeployConfig, "domain"))
res, err := q.req(qiniuGateway+path, http.MethodGet, nil)
if err != nil {
@@ -135,8 +135,8 @@ func (q *qiuniu) uploadCert() (string, error) {
path := "/sslcert"
body := &uploadCertReq{
Name: q.option.Domain,
CommonName: q.option.Domain,
Name: getDeployString(q.option.DeployConfig, "domain"),
CommonName: getDeployString(q.option.DeployConfig, "domain"),
Pri: q.option.Certificate.PrivateKey,
Ca: q.option.Certificate.Certificate,
}
@@ -166,7 +166,7 @@ type modifyDomainCertReq struct {
}
func (q *qiuniu) modifyDomainCert(certId string) error {
path := fmt.Sprintf("/domain/%s/httpsconf", q.option.Domain)
path := fmt.Sprintf("/domain/%s/httpsconf", getDeployString(q.option.DeployConfig, "domain"))
body := &modifyDomainCertReq{
CertID: certId,

View File

@@ -7,7 +7,6 @@ import (
"fmt"
"os"
xpath "path"
"strings"
"github.com/pkg/sftp"
sshPkg "golang.org/x/crypto/ssh"
@@ -19,15 +18,11 @@ type ssh struct {
}
type sshAccess struct {
Host string `json:"host"`
Username string `json:"username"`
Password string `json:"password"`
Key string `json:"key"`
Port string `json:"port"`
PreCommand string `json:"preCommand"`
Command string `json:"command"`
CertPath string `json:"certPath"`
KeyPath string `json:"keyPath"`
Host string `json:"host"`
Username string `json:"username"`
Password string `json:"password"`
Key string `json:"key"`
Port string `json:"port"`
}
func NewSSH(option *DeployerOption) (Deployer, error) {
@@ -50,16 +45,6 @@ func (s *ssh) Deploy(ctx context.Context) error {
if err := json.Unmarshal([]byte(s.option.Access), access); err != nil {
return err
}
// 将证书路径和命令中的变量替换为实际值
for k, v := range s.option.Variables {
key := fmt.Sprintf("${%s}", k)
access.CertPath = strings.ReplaceAll(access.CertPath, key, v)
access.KeyPath = strings.ReplaceAll(access.KeyPath, key, v)
access.Command = strings.ReplaceAll(access.Command, key, v)
access.PreCommand = strings.ReplaceAll(access.PreCommand, key, v)
}
// 连接
client, err := s.getClient(access)
if err != nil {
@@ -70,29 +55,30 @@ func (s *ssh) Deploy(ctx context.Context) error {
s.infos = append(s.infos, toStr("ssh连接成功", nil))
// 执行前置命令
if access.PreCommand != "" {
err, stdout, stderr := s.sshExecCommand(client, access.PreCommand)
preCommand := getDeployString(s.option.DeployConfig, "preCommand")
if preCommand != "" {
err, stdout, stderr := s.sshExecCommand(client, preCommand)
if err != nil {
return fmt.Errorf("failed to run pre-command: %w, stdout: %s, stderr: %s", err, stdout, stderr)
}
}
// 上传证书
if err := s.upload(client, s.option.Certificate.Certificate, access.CertPath); err != nil {
if err := s.upload(client, s.option.Certificate.Certificate, getDeployString(s.option.DeployConfig, "certPath")); err != nil {
return fmt.Errorf("failed to upload certificate: %w", err)
}
s.infos = append(s.infos, toStr("ssh上传证书成功", nil))
// 上传私钥
if err := s.upload(client, s.option.Certificate.PrivateKey, access.KeyPath); err != nil {
if err := s.upload(client, s.option.Certificate.PrivateKey, getDeployString(s.option.DeployConfig, "keyPath")); err != nil {
return fmt.Errorf("failed to upload private key: %w", err)
}
s.infos = append(s.infos, toStr("ssh上传私钥成功", nil))
// 执行命令
err, stdout, stderr := s.sshExecCommand(client, access.Command)
err, stdout, stderr := s.sshExecCommand(client, getDeployString(s.option.DeployConfig, "command"))
if err != nil {
return fmt.Errorf("failed to run command: %w, stdout: %s, stderr: %s", err, stdout, stderr)
}

View File

@@ -14,9 +14,10 @@ type webhookAccess struct {
}
type hookData struct {
Domain string `json:"domain"`
Certificate string `json:"certificate"`
PrivateKey string `json:"privateKey"`
Domain string `json:"domain"`
Certificate string `json:"certificate"`
PrivateKey string `json:"privateKey"`
Variables map[string]string `json:"variables"`
}
type webhook struct {
@@ -50,6 +51,7 @@ func (w *webhook) Deploy(ctx context.Context) error {
Domain: w.option.Domain,
Certificate: w.option.Certificate.Certificate,
PrivateKey: w.option.Certificate.PrivateKey,
Variables: getDeployVariables(w.option.DeployConfig),
}
body, _ := json.Marshal(data)

View File

@@ -0,0 +1,20 @@
package domain
type ApplyConfig struct {
Email string `json:"email"`
Access string `json:"access"`
Timeout int64 `json:"timeout"`
Nameservers string `json:"nameservers"`
}
type DeployConfig struct {
Id string `json:"id"`
Access string `json:"access"`
Type string `json:"type"`
Config map[string]any `json:"config"`
}
type KV struct {
Key string `json:"key"`
Value string `json:"value"`
}

View File

@@ -5,7 +5,6 @@ import (
"certimate/internal/deployer"
"certimate/internal/utils/app"
"context"
"errors"
"fmt"
"time"
@@ -41,18 +40,6 @@ func deploy(ctx context.Context, record *models.Record) error {
return err
}
history.record(checkPhase, "获取记录成功", nil)
if errs := app.GetApp().Dao().ExpandRecord(currRecord, []string{"access", "targetAccess", "group"}, nil); len(errs) > 0 {
errList := make([]error, 0)
for name, err := range errs {
errList = append(errList, fmt.Errorf("展开记录失败,%s: %w", name, err))
}
err = errors.Join(errList...)
app.GetApp().Logger().Error("展开记录失败", "err", err)
history.record(checkPhase, "获取授权信息失败", &RecordInfo{Err: err})
return err
}
history.record(checkPhase, "获取授权信息成功", nil)
cert := currRecord.GetString("certificate")
expiredAt := currRecord.GetDateTime("expiredAt").Time()
@@ -106,6 +93,13 @@ func deploy(ctx context.Context, record *models.Record) error {
return err
}
// 没有部署配置,也算成功
if len(deployers) == 0 {
history.record(deployPhase, "没有部署配置", &RecordInfo{Info: []string{"没有部署配置"}})
history.setWholeSuccess(true)
return nil
}
for _, deployer := range deployers {
if err = deployer.Deploy(ctx); err != nil {