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