Files
certimate/internal/workflow/engine/executor_bizapply.go
T
2025-09-05 21:05:03 +08:00

364 lines
12 KiB
Go

package engine
import (
"fmt"
"log/slog"
"maps"
"math"
"os"
"slices"
"strings"
"time"
"github.com/go-acme/lego/v4/certcrypto"
legocertifier "github.com/go-acme/lego/v4/certificate"
"github.com/go-acme/lego/v4/lego"
legolog "github.com/go-acme/lego/v4/log"
"github.com/samber/lo"
"github.com/xhit/go-str2duration/v2"
"github.com/certimate-go/certimate/internal/app"
"github.com/certimate-go/certimate/internal/certapply"
"github.com/certimate-go/certimate/internal/domain"
"github.com/certimate-go/certimate/internal/repository"
"github.com/certimate-go/certimate/internal/tools/mproc"
xcert "github.com/certimate-go/certimate/pkg/utils/cert"
)
var useMultiProc = true
func init() {
envMultiProc := os.Getenv("CERTIMATE_WORKFLOW_MULTIPROC")
if envMultiProc == "0" {
useMultiProc = false
}
}
/**
* Result Variables:
* - node.skipped: boolean
* - certificate.validity: boolean
* - certificate.daysLeft: number
*
* Result Outputs:
* - ref: certificate: string
*/
type bizApplyNodeExecutor struct {
nodeExecutor
accessRepo accessRepository
certificateRepo certificateRepository
wfoutputRepo workflowOutputRepository
}
func (ne *bizApplyNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeExecutionResult, error) {
execRes := newNodeExecutionResult(execCtx.Node)
nodeCfg := execCtx.Node.Data.Config.AsBizApply()
ne.logger.Info("ready to request certificate ...", slog.Any("config", nodeCfg))
// 查询上次执行结果
lastOutput, lastCertificate, err := ne.getLastOutputArtifacts(execCtx)
if err != nil {
return execRes, err
} else if lastCertificate != nil {
execRes.AddOutput(stateIOTypeRef, "certificate", fmt.Sprintf("%s#%s", domain.CollectionNameCertificate, lastCertificate.Id), "string")
execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateValidity, time.Now().After(lastCertificate.ValidityNotAfter), "boolean")
execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateDaysLeft, int32(time.Until(lastCertificate.ValidityNotAfter).Hours()/24), "number")
}
// 检测是否可以跳过本次执行
if skippable, reason := ne.checkCanSkip(execCtx, lastOutput, lastCertificate); skippable {
ne.logger.Info(fmt.Sprintf("skip this application, because %s", reason))
execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyNodeSkipped, true, "boolean")
return execRes, nil
} else if reason != "" {
ne.logger.Info(fmt.Sprintf("re-apply, because %s", reason))
} else {
ne.logger.Info("no found last issued certificate, begin to apply")
}
obtainResp, err := ne.executeObtain(execCtx, &nodeCfg, lastCertificate)
if err != nil {
return execRes, err
}
// 解析证书
certX509, err := xcert.ParseCertificateFromPEM(obtainResp.FullChainCertificate)
if err != nil {
ne.logger.Warn("could not parse certificate, may be the CA responded error")
return execRes, err
}
// 保存证书实体
certificate := &domain.Certificate{
Source: domain.CertificateSourceTypeRequest,
Certificate: obtainResp.FullChainCertificate,
PrivateKey: obtainResp.PrivateKey,
IssuerCertificate: obtainResp.IssuerCertificate,
ACMEAcctUrl: obtainResp.ACMEAcctUrl,
ACMECertUrl: obtainResp.ACMECertUrl,
ACMECertStableUrl: obtainResp.ACMECertStableUrl,
WorkflowId: execCtx.WorkflowId,
WorkflowRunId: execCtx.RunId,
WorkflowNodeId: execCtx.Node.Id,
}
certificate.PopulateFromX509(certX509)
if certificate, err := ne.certificateRepo.Save(execCtx.ctx, certificate); err != nil {
ne.logger.Warn("could not save certificate")
return execRes, err
} else {
ne.logger.Info("certificate saved", slog.String("recordId", certificate.Id))
}
// 保存 ARI 记录
if lastCertificate != nil && obtainResp.ARIReplaced {
lastCertificate.ACMERenewed = true
ne.certificateRepo.Save(execCtx.ctx, lastCertificate)
}
// 节点输出
execRes.AddOutputWithPersistent(stateIOTypeRef, "certificate", fmt.Sprintf("%s#%s", domain.CollectionNameCertificate, certificate.Id), "string")
execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyNodeSkipped, false, "boolean")
execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateValidity, true, "boolean")
execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateDaysLeft, int32(time.Until(certificate.ValidityNotAfter).Hours()/24), "number")
ne.logger.Info("application completed")
return execRes, nil
}
func (ne *bizApplyNodeExecutor) getLastOutputArtifacts(execCtx *NodeExecutionContext) (*domain.WorkflowOutput, *domain.Certificate, error) {
lastOutput, err := ne.wfoutputRepo.GetByNodeId(execCtx.ctx, execCtx.Node.Id)
if err != nil && !domain.IsRecordNotFoundError(err) {
return nil, nil, fmt.Errorf("failed to get last output record of node #%s: %w", execCtx.Node.Id, err)
}
if lastOutput != nil {
lastCertificate, err := ne.certificateRepo.GetByWorkflowRunIdAndNodeId(execCtx.ctx, lastOutput.RunId, lastOutput.NodeId)
if err != nil && !domain.IsRecordNotFoundError(err) {
return lastOutput, nil, fmt.Errorf("failed to get last certificate record of node #%s: %w", execCtx.Node.Id, err)
}
return lastOutput, lastCertificate, nil
}
return lastOutput, nil, nil
}
func (ne *bizApplyNodeExecutor) checkCanSkip(execCtx *NodeExecutionContext, lastOutput *domain.WorkflowOutput, lastCertificate *domain.Certificate) (_skip bool, _reason string) {
thisNodeCfg := execCtx.Node.Data.Config.AsBizApply()
if lastOutput != nil && lastOutput.Succeeded {
// 比较和上次申请时的关键配置(即影响证书签发的)参数是否一致
lastNodeCfg := lastOutput.NodeConfig.AsBizApply()
if !slices.Equal(thisNodeCfg.Domains, lastNodeCfg.Domains) {
return false, "the configuration item 'Domains' changed"
}
if thisNodeCfg.ContactEmail != lastNodeCfg.ContactEmail {
return false, "the configuration item 'ContactEmail' changed"
}
if thisNodeCfg.Provider != lastNodeCfg.Provider {
return false, "the configuration item 'Provider' changed"
}
if thisNodeCfg.ProviderAccessId != lastNodeCfg.ProviderAccessId {
return false, "the configuration item 'ProviderAccessId' changed"
}
if !maps.Equal(thisNodeCfg.ProviderConfig, lastNodeCfg.ProviderConfig) {
return false, "the configuration item 'ProviderConfig' changed"
}
if thisNodeCfg.CAProvider != lastNodeCfg.CAProvider {
return false, "the configuration item 'CAProvider' changed"
}
if thisNodeCfg.CAProviderAccessId != lastNodeCfg.CAProviderAccessId {
return false, "the configuration item 'CAProviderAccessId' changed"
}
if !maps.Equal(thisNodeCfg.CAProviderConfig, lastNodeCfg.CAProviderConfig) {
return false, "the configuration item 'CAProviderConfig' changed"
}
if thisNodeCfg.KeyAlgorithm != lastNodeCfg.KeyAlgorithm {
return false, "the configuration item 'KeyAlgorithm' changed"
}
}
if lastCertificate != nil {
renewalInterval := time.Duration(thisNodeCfg.SkipBeforeExpiryDays) * time.Hour * 24
expirationTime := time.Until(lastCertificate.ValidityNotAfter)
daysLeft := int(math.Floor(expirationTime.Hours() / 24))
if expirationTime > renewalInterval {
return true, fmt.Sprintf("the last issued certificate #%s expires in %d day(s), next renewal will be in %d day(s)", lastCertificate.Id, daysLeft, thisNodeCfg.SkipBeforeExpiryDays)
}
return false, fmt.Sprintf("the last issued certificate #%s expires in %d day(s)", lastCertificate.Id, daysLeft)
}
return false, ""
}
func (ne *bizApplyNodeExecutor) executeObtain(execCtx *NodeExecutionContext, nodeCfg *domain.WorkflowNodeConfigForBizApply, lastCertificate *domain.Certificate) (*certapply.ObtainCertificateResponse, error) {
// 读取证书算法
legoKeyType, err := domain.CertificateKeyAlgorithmType(nodeCfg.KeyAlgorithm).KeyType()
if err != nil {
return nil, err
}
// 读取质询提供商授权
providerAccessConfig := make(map[string]any)
if nodeCfg.ProviderAccessId != "" {
if access, err := ne.accessRepo.GetById(execCtx.ctx, nodeCfg.ProviderAccessId); err != nil {
return nil, fmt.Errorf("failed to get access #%s record: %w", nodeCfg.ProviderAccessId, err)
} else {
providerAccessConfig = access.Config
}
}
// 读取证书颁发机构授权
caAccessConfig := make(map[string]any)
if nodeCfg.CAProviderAccessId != "" {
if access, err := ne.accessRepo.GetById(execCtx.ctx, nodeCfg.CAProviderAccessId); err != nil {
return nil, fmt.Errorf("failed to get access #%s record: %w", nodeCfg.CAProviderAccessId, err)
} else {
caAccessConfig = access.Config
}
}
// 初始化 ACME 配置项
legoOptions := &certapply.ACMEConfigOptions{
CAProvider: nodeCfg.CAProvider,
CAAccessConfig: caAccessConfig,
CAProviderConfig: nodeCfg.CAProviderConfig,
CertifierKeyType: legoKeyType,
}
legoConfig, err := certapply.NewACMEConfig(legoOptions)
if err != nil {
ne.logger.Warn("could not initialize acme config")
return nil, err
} else {
ne.logger.Info("acme config initialized", slog.String("acmeDirUrl", legoConfig.CADirUrl))
}
// 初始化 ACME 账户
// 注意此步骤仍需在主进程中进行,以保证并发安全
legoUser, err := certapply.NewACMEAccountWithSingleFlight(legoConfig, nodeCfg.ContactEmail)
if err != nil {
ne.logger.Warn("could not initialize acme account")
return nil, err
} else {
ne.logger.Info("acme account initialized", slog.String("acmeAcctUrl", legoUser.ACMEAcctUrl))
}
// 构造证书申请请求
obtainReq := &certapply.ObtainCertificateRequest{
Domains: nodeCfg.Domains,
KeyType: legoKeyType,
ChallengeType: nodeCfg.ChallengeType,
Provider: nodeCfg.Provider,
ProviderAccessConfig: providerAccessConfig,
ProviderExtendedConfig: nodeCfg.ProviderConfig,
DisableFollowCNAME: nodeCfg.DisableFollowCNAME,
Nameservers: nodeCfg.Nameservers,
DnsPropagationWait: nodeCfg.DnsPropagationWait,
DnsPropagationTimeout: nodeCfg.DnsPropagationTimeout,
DnsTTL: nodeCfg.DnsTTL,
HttpDelayWait: nodeCfg.HttpDelayWait,
ValidityTo: lo.If(nodeCfg.ValidityLifetime == "", time.Time{}).
ElseF(func() time.Time {
duration, err := str2duration.ParseDuration(nodeCfg.ValidityLifetime)
if err != nil {
return time.Time{}
}
return time.Now().Add(duration)
}),
ACMEProfile: nodeCfg.ACMEProfile,
ARIReplacesAcctUrl: lo.If(lastCertificate == nil, "").
ElseF(func() string {
if lastCertificate.ACMERenewed {
return ""
}
return lastCertificate.ACMEAcctUrl
}),
ARIReplacesCertId: lo.If(lastCertificate == nil, "").
ElseF(func() string {
if lastCertificate.ACMERenewed {
return ""
}
newCertSan := slices.Clone(nodeCfg.Domains)
oldCertSan := strings.Split(lastCertificate.SubjectAltNames, ";")
slices.Sort(newCertSan)
slices.Sort(oldCertSan)
if !slices.Equal(newCertSan, oldCertSan) {
return ""
}
oldCertX509, err := certcrypto.ParsePEMCertificate([]byte(lastCertificate.Certificate))
if err != nil {
return ""
}
oldARICertId, _ := legocertifier.MakeARICertID(oldCertX509)
return oldARICertId
}),
}
// 如果启用多进程模式,发送指令
if useMultiProc {
type InData struct {
Account *certapply.ACMEAccount `json:"account,omitempty"`
Request *certapply.ObtainCertificateRequest `json:"request,omitempty"`
}
type OutData struct {
Response *certapply.ObtainCertificateResponse `json:"response"`
}
msender := mproc.NewSender[InData, OutData]("certapply", ne.logger)
moutput, err := msender.SendWithContext(execCtx.ctx, &InData{
Account: legoUser,
Request: obtainReq,
})
if err != nil {
ne.logger.Warn("could not obtain certificate")
return nil, err
}
if moutput.Response != nil {
return moutput.Response, nil
} else {
panic("impossible!")
}
}
// 初始化 ACME 客户端
legolog.Logger = certapply.NewLegoLogger(app.GetLogger())
legoClient, err := certapply.NewACMEClientWithAccount(legoUser, func(c *lego.Config) error {
c.UserAgent = "certimate"
c.Certificate.KeyType = legoKeyType
return nil
})
if err != nil {
ne.logger.Warn("could not initialize acme client")
return nil, err
}
// 执行申请证书请求
obtainResp, err := legoClient.ObtainCertificate(execCtx.ctx, obtainReq)
if err != nil {
ne.logger.Warn("could not obtain certificate")
return nil, err
}
return obtainResp, nil
}
func newBizApplyNodeExecutor() NodeExecutor {
return &bizApplyNodeExecutor{
nodeExecutor: nodeExecutor{logger: slog.Default()},
accessRepo: repository.NewAccessRepository(),
certificateRepo: repository.NewCertificateRepository(),
wfoutputRepo: repository.NewWorkflowOutputRepository(),
}
}