支持续期策略运行时生效
This commit is contained in:
parent
8ab3459a03
commit
11cbd8a2f2
@ -138,10 +138,11 @@ func NewWithConfigPath(ctx context.Context, cfg config.Config, configPath string
|
||||
RunRenewal: func(ctx context.Context) (certificate.RenewalReport, error) {
|
||||
return renewal.RunOnce(ctx)
|
||||
},
|
||||
CurrentCertificate: certificates,
|
||||
ResetStatePath: admin.StatePath(cfg.Paths.Data),
|
||||
Audit: auditRepo,
|
||||
Tasks: taskRepo,
|
||||
UpdateRenewalPolicy: renewal.UpdatePolicy,
|
||||
CurrentCertificate: certificates,
|
||||
ResetStatePath: admin.StatePath(cfg.Paths.Data),
|
||||
Audit: auditRepo,
|
||||
Tasks: taskRepo,
|
||||
})
|
||||
return &App{
|
||||
Config: cfg, Logger: logger, Database: db, Repository: repo,
|
||||
|
||||
@ -28,13 +28,15 @@ type RenewalPolicy struct {
|
||||
}
|
||||
|
||||
type RenewalScheduler struct {
|
||||
lister DomainLister
|
||||
issuer CertificateIssuer
|
||||
policy RenewalPolicy
|
||||
now func() time.Time
|
||||
mu sync.Mutex
|
||||
runMu sync.Mutex
|
||||
running bool
|
||||
lister DomainLister
|
||||
issuer CertificateIssuer
|
||||
policy RenewalPolicy
|
||||
now func() time.Time
|
||||
mu sync.Mutex
|
||||
policyMu sync.RWMutex
|
||||
runMu sync.Mutex
|
||||
running bool
|
||||
wake chan struct{}
|
||||
}
|
||||
|
||||
type RenewalReport struct {
|
||||
@ -63,7 +65,32 @@ func NewRenewalScheduler(lister DomainLister, issuer CertificateIssuer, threshol
|
||||
|
||||
func NewRenewalSchedulerWithPolicy(lister DomainLister, issuer CertificateIssuer, policy RenewalPolicy) *RenewalScheduler {
|
||||
policy = normalizeRenewalPolicy(policy)
|
||||
return &RenewalScheduler{lister: lister, issuer: issuer, policy: policy, now: time.Now}
|
||||
return &RenewalScheduler{lister: lister, issuer: issuer, policy: policy, now: time.Now, wake: make(chan struct{}, 1)}
|
||||
}
|
||||
|
||||
func (s *RenewalScheduler) UpdatePolicy(policy RenewalPolicy) {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
policy = normalizeRenewalPolicy(policy)
|
||||
s.policyMu.Lock()
|
||||
s.policy = policy
|
||||
s.policyMu.Unlock()
|
||||
if s.wake != nil {
|
||||
select {
|
||||
case s.wake <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *RenewalScheduler) Policy() RenewalPolicy {
|
||||
if s == nil {
|
||||
return normalizeRenewalPolicy(RenewalPolicy{})
|
||||
}
|
||||
s.policyMu.RLock()
|
||||
defer s.policyMu.RUnlock()
|
||||
return s.policy
|
||||
}
|
||||
|
||||
func (s *RenewalScheduler) RunOnce(ctx context.Context) (RenewalReport, error) {
|
||||
@ -79,24 +106,25 @@ func (s *RenewalScheduler) RunOnce(ctx context.Context) (RenewalReport, error) {
|
||||
return RenewalReport{}, fmt.Errorf("查询续期域名失败:%w", err)
|
||||
}
|
||||
report := RenewalReport{Checked: len(items)}
|
||||
policy := s.Policy()
|
||||
now := s.now().UTC()
|
||||
for _, item := range items {
|
||||
if !item.AutoRenew || item.Status == "issuing" {
|
||||
continue
|
||||
}
|
||||
remaining := renewalRemaining(item.ExpiresAt, now)
|
||||
if item.ExpiresAt != nil && remaining <= s.policy.NearExpiryThreshold {
|
||||
if item.ExpiresAt != nil && remaining <= policy.NearExpiryThreshold {
|
||||
report.NearExpiry++
|
||||
}
|
||||
if item.ExpiresAt != nil && remaining > s.policy.RenewalThreshold {
|
||||
if item.ExpiresAt != nil && remaining > policy.RenewalThreshold {
|
||||
continue
|
||||
}
|
||||
report.Selected++
|
||||
urgent := item.ExpiresAt != nil && remaining <= s.policy.UrgentThreshold
|
||||
urgent := item.ExpiresAt != nil && remaining <= policy.UrgentThreshold
|
||||
if urgent {
|
||||
report.Urgent++
|
||||
}
|
||||
if err := s.issueWithRetry(ctx, item.ID, urgent); err != nil {
|
||||
if err := s.issueWithRetry(ctx, item.ID, urgent, policy); err != nil {
|
||||
report.Failed++
|
||||
report.Errors = append(report.Errors, fmt.Sprintf("%s:%v", item.Name, err))
|
||||
continue
|
||||
@ -125,7 +153,7 @@ func (s *RenewalScheduler) Start(ctx context.Context) {
|
||||
}()
|
||||
report, err := s.RunOnce(ctx)
|
||||
for {
|
||||
interval := s.policy.CheckInterval
|
||||
interval := s.Policy().CheckInterval
|
||||
if err == nil && report.NearExpiry > 0 {
|
||||
interval = 24 * time.Hour
|
||||
}
|
||||
@ -137,16 +165,20 @@ func (s *RenewalScheduler) Start(ctx context.Context) {
|
||||
}
|
||||
return
|
||||
case <-timer.C:
|
||||
case <-s.wake:
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
report, err = s.RunOnce(ctx)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *RenewalScheduler) issueWithRetry(ctx context.Context, domainID string, urgent bool) error {
|
||||
func (s *RenewalScheduler) issueWithRetry(ctx context.Context, domainID string, urgent bool, policy RenewalPolicy) error {
|
||||
attempts := 1
|
||||
if urgent {
|
||||
attempts = s.policy.MaxRetries
|
||||
attempts = policy.MaxRetries
|
||||
}
|
||||
var lastErr error
|
||||
for attempt := 1; attempt <= attempts; attempt++ {
|
||||
@ -158,7 +190,7 @@ func (s *RenewalScheduler) issueWithRetry(ctx context.Context, domainID string,
|
||||
if attempt == attempts {
|
||||
break
|
||||
}
|
||||
if err := waitForRenewalRetry(ctx, s.policy.RetryInterval); err != nil {
|
||||
if err := waitForRenewalRetry(ctx, policy.RetryInterval); err != nil {
|
||||
return fmt.Errorf("第 %d 次续期失败,等待重试被取消:%w", attempt, err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,11 +51,12 @@ type Dependencies struct {
|
||||
DeployCertificate func(context.Context, string) (certificate.IssueResult, deployment.Report, error)
|
||||
RestoreCertificateVersion func(context.Context, string, string) (certificate.IssueResult, error)
|
||||
|
||||
RunRenewal func(context.Context) (certificate.RenewalReport, error)
|
||||
CurrentCertificate certificateReader
|
||||
ResetStatePath string
|
||||
Audit repository.AuditRepository
|
||||
Tasks repository.TaskRepository
|
||||
RunRenewal func(context.Context) (certificate.RenewalReport, error)
|
||||
UpdateRenewalPolicy func(certificate.RenewalPolicy)
|
||||
CurrentCertificate certificateReader
|
||||
ResetStatePath string
|
||||
Audit repository.AuditRepository
|
||||
Tasks repository.TaskRepository
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
@ -78,24 +79,25 @@ type Server struct {
|
||||
deployCertificate func(context.Context, string) (certificate.IssueResult, deployment.Report, error)
|
||||
restoreVersion func(context.Context, string, string) (certificate.IssueResult, error)
|
||||
|
||||
runRenewal func(context.Context) (certificate.RenewalReport, error)
|
||||
currentCertificate certificateReader
|
||||
resetStatePath string
|
||||
audit repository.AuditRepository
|
||||
tasks repository.TaskRepository
|
||||
taskMu sync.Mutex
|
||||
taskCancels map[string]context.CancelFunc
|
||||
issueTaskMu sync.Mutex
|
||||
resetMu sync.Mutex
|
||||
resetClosedHash string
|
||||
resetLimiter *resetRateLimiter
|
||||
authSessions map[string]authSession
|
||||
authMu sync.RWMutex
|
||||
loginAttempts map[string]loginAttempt
|
||||
loginMu sync.Mutex
|
||||
now func() time.Time
|
||||
httpServer *http.Server
|
||||
router *gin.Engine
|
||||
runRenewal func(context.Context) (certificate.RenewalReport, error)
|
||||
updateRenewalPolicy func(certificate.RenewalPolicy)
|
||||
currentCertificate certificateReader
|
||||
resetStatePath string
|
||||
audit repository.AuditRepository
|
||||
tasks repository.TaskRepository
|
||||
taskMu sync.Mutex
|
||||
taskCancels map[string]context.CancelFunc
|
||||
issueTaskMu sync.Mutex
|
||||
resetMu sync.Mutex
|
||||
resetClosedHash string
|
||||
resetLimiter *resetRateLimiter
|
||||
authSessions map[string]authSession
|
||||
authMu sync.RWMutex
|
||||
loginAttempts map[string]loginAttempt
|
||||
loginMu sync.Mutex
|
||||
now func() time.Time
|
||||
httpServer *http.Server
|
||||
router *gin.Engine
|
||||
}
|
||||
|
||||
func New(deps Dependencies) *Server {
|
||||
@ -119,6 +121,7 @@ func New(deps Dependencies) *Server {
|
||||
deployCertificate: deps.DeployCertificate,
|
||||
restoreVersion: deps.RestoreCertificateVersion,
|
||||
runRenewal: deps.RunRenewal,
|
||||
updateRenewalPolicy: deps.UpdateRenewalPolicy,
|
||||
currentCertificate: deps.CurrentCertificate,
|
||||
resetStatePath: deps.ResetStatePath,
|
||||
audit: deps.Audit,
|
||||
|
||||
@ -6,9 +6,11 @@ import (
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/certificate"
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/config"
|
||||
)
|
||||
|
||||
@ -212,16 +214,22 @@ func (s *Server) updateSettings(c *gin.Context) {
|
||||
s.writeError(c, http.StatusInternalServerError, "保存设置失败", err)
|
||||
return
|
||||
}
|
||||
restartRequired := previous.Server.Host != updated.Server.Host ||
|
||||
listenerRestartRequired := previous.Server.Host != updated.Server.Host ||
|
||||
previous.Server.Port != updated.Server.Port ||
|
||||
previous.Server.HTTPSMode != updated.Server.HTTPSMode ||
|
||||
previous.Server.HTTPSMode != updated.Server.HTTPSMode
|
||||
configRestartRequired := listenerRestartRequired ||
|
||||
previous.Paths.Certificate != updated.Paths.Certificate ||
|
||||
!reflect.DeepEqual(previous.ACME, updated.ACME) ||
|
||||
!reflect.DeepEqual(nonRenewalACMEConfig(previous.ACME), nonRenewalACMEConfig(updated.ACME)) ||
|
||||
!reflect.DeepEqual(previous.DNS, updated.DNS) ||
|
||||
!reflect.DeepEqual(previous.Notification, updated.Notification)
|
||||
if s.updateRenewalPolicy != nil {
|
||||
s.updateRenewalPolicy(renewalPolicyFromConfig(updated))
|
||||
}
|
||||
message := "设置已保存"
|
||||
if restartRequired {
|
||||
if listenerRestartRequired {
|
||||
message = "设置已保存;服务监听地址、端口或 HTTPS 模式已变更,请重启 Scert 服务"
|
||||
} else if configRestartRequired {
|
||||
message = "设置已保存;部分证书、DNS 或通知运行配置已变更,请重启 Scert 服务"
|
||||
}
|
||||
s.config = updated
|
||||
s.configMu.Unlock()
|
||||
@ -258,7 +266,7 @@ func (s *Server) updateSettings(c *gin.Context) {
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"settings": settingsViewFromConfig(updated),
|
||||
"restart_required": restartRequired,
|
||||
"restart_required": configRestartRequired,
|
||||
"message": message,
|
||||
})
|
||||
}
|
||||
@ -296,6 +304,27 @@ func (s *Server) applySettingsDNS(ctx context.Context, cfg *config.Config, previ
|
||||
return credentialRef, verificationToken, testDomain, nil
|
||||
}
|
||||
|
||||
func renewalPolicyFromConfig(cfg config.Config) certificate.RenewalPolicy {
|
||||
return certificate.RenewalPolicy{
|
||||
CheckInterval: time.Duration(cfg.ACME.RenewalCheckIntervalDays) * 24 * time.Hour,
|
||||
NearExpiryThreshold: time.Duration(cfg.ACME.RenewalNearExpiryDays) * 24 * time.Hour,
|
||||
RenewalThreshold: time.Duration(cfg.ACME.RenewalThresholdDays) * 24 * time.Hour,
|
||||
UrgentThreshold: time.Duration(cfg.ACME.RenewalUrgentThresholdDays) * 24 * time.Hour,
|
||||
MaxRetries: cfg.ACME.RenewalMaxRetries,
|
||||
RetryInterval: time.Duration(cfg.ACME.RenewalRetryIntervalMins) * time.Minute,
|
||||
}
|
||||
}
|
||||
|
||||
func nonRenewalACMEConfig(cfg config.ACMEConfig) config.ACMEConfig {
|
||||
cfg.RenewalCheckIntervalDays = 0
|
||||
cfg.RenewalNearExpiryDays = 0
|
||||
cfg.RenewalThresholdDays = 0
|
||||
cfg.RenewalUrgentThresholdDays = 0
|
||||
cfg.RenewalMaxRetries = 0
|
||||
cfg.RenewalRetryIntervalMins = 0
|
||||
return cfg
|
||||
}
|
||||
|
||||
func settingsViewFromConfig(cfg config.Config) settingsView {
|
||||
return settingsView{
|
||||
Server: settingsServerView{Host: cfg.Server.Host, Port: cfg.Server.Port, HTTPSMode: cfg.Server.HTTPSMode},
|
||||
|
||||
@ -9,6 +9,7 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
@ -140,7 +141,16 @@ func TestSettingsAPIReturnsAndUpdatesRenewalPolicy(t *testing.T) {
|
||||
if err := cfg.Save(configPath); err != nil {
|
||||
t.Fatalf("保存测试配置失败:%v", err)
|
||||
}
|
||||
server := New(Dependencies{Config: cfg, ConfigPath: configPath})
|
||||
var applied certificate.RenewalPolicy
|
||||
appliedCalled := false
|
||||
server := New(Dependencies{
|
||||
Config: cfg,
|
||||
ConfigPath: configPath,
|
||||
UpdateRenewalPolicy: func(policy certificate.RenewalPolicy) {
|
||||
applied = policy
|
||||
appliedCalled = true
|
||||
},
|
||||
})
|
||||
cookie := loginForSettingsTest(t, server)
|
||||
|
||||
getRequest := httptest.NewRequest(http.MethodGet, "/api/v1/settings", nil)
|
||||
@ -184,9 +194,15 @@ func TestSettingsAPIReturnsAndUpdatesRenewalPolicy(t *testing.T) {
|
||||
if err := json.Unmarshal(response.Body.Bytes(), &payload); err != nil {
|
||||
t.Fatalf("解析续期策略响应失败:%v", err)
|
||||
}
|
||||
if !payload.Restart || payload.Settings.ACME.RenewalCheckIntervalDays != 2 || payload.Settings.ACME.RenewalNearExpiryDays != 14 || payload.Settings.ACME.RenewalThresholdDays != 7 || payload.Settings.ACME.RenewalUrgentThresholdDays != 2 || payload.Settings.ACME.RenewalMaxRetries != 5 || payload.Settings.ACME.RenewalRetryIntervalMins != 45 {
|
||||
if payload.Restart || payload.Settings.ACME.RenewalCheckIntervalDays != 2 || payload.Settings.ACME.RenewalNearExpiryDays != 14 || payload.Settings.ACME.RenewalThresholdDays != 7 || payload.Settings.ACME.RenewalUrgentThresholdDays != 2 || payload.Settings.ACME.RenewalMaxRetries != 5 || payload.Settings.ACME.RenewalRetryIntervalMins != 45 {
|
||||
t.Fatalf("更新后的续期策略不正确:restart=%v acme=%+v", payload.Restart, payload.Settings.ACME)
|
||||
}
|
||||
if !appliedCalled {
|
||||
t.Fatal("\u4fdd\u5b58\u7eed\u671f\u7b56\u7565\u540e\u5e94\u7acb\u5373\u8c03\u7528\u8fd0\u884c\u65f6\u66f4\u65b0\u56de\u8c03")
|
||||
}
|
||||
if applied.CheckInterval != 2*24*time.Hour || applied.NearExpiryThreshold != 14*24*time.Hour || applied.RenewalThreshold != 7*24*time.Hour || applied.UrgentThreshold != 2*24*time.Hour || applied.MaxRetries != 5 || applied.RetryInterval != 45*time.Minute {
|
||||
t.Fatalf("\u8fd0\u884c\u65f6\u7eed\u671f\u7b56\u7565\u672a\u6b63\u786e\u8f6c\u6362\uff1a%+v", applied)
|
||||
}
|
||||
saved, err := config.Load(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("读取续期策略配置失败:%v", err)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user