完善自动续期策略与管理配置
This commit is contained in:
parent
8158cefc05
commit
4ac123798d
@ -106,7 +106,14 @@ func NewWithConfigPath(ctx context.Context, cfg config.Config, configPath string
|
|||||||
NotifyError: func(err error) { logger.Error("发送邮件通知失败", "error", err) },
|
NotifyError: func(err error) { logger.Error("发送邮件通知失败", "error", err) },
|
||||||
CurrentCertificate: certificates.Current,
|
CurrentCertificate: certificates.Current,
|
||||||
}
|
}
|
||||||
renewal := certificate.NewRenewalScheduler(repo, workflow, cfg.ACME.RenewalThresholdDays)
|
renewal := certificate.NewRenewalSchedulerWithPolicy(repo, workflow, 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,
|
||||||
|
})
|
||||||
server := httpapi.New(httpapi.Dependencies{
|
server := httpapi.New(httpapi.Dependencies{
|
||||||
Config: cfg, ConfigPath: configPath, Logger: logger, Repository: repo, Servers: serverRepo, Users: userRepo, SSHKeys: sshKeys,
|
Config: cfg, ConfigPath: configPath, Logger: logger, Repository: repo, Servers: serverRepo, Users: userRepo, SSHKeys: sshKeys,
|
||||||
SetupDNS: dnsVerification, Secrets: secretStore, Firewall: firewall.NewManager(),
|
SetupDNS: dnsVerification, Secrets: secretStore, Firewall: firewall.NewManager(),
|
||||||
|
|||||||
@ -18,33 +18,52 @@ type CertificateIssuer interface {
|
|||||||
Issue(ctx context.Context, domainID string) (IssueResult, error)
|
Issue(ctx context.Context, domainID string) (IssueResult, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RenewalPolicy struct {
|
||||||
|
CheckInterval time.Duration
|
||||||
|
NearExpiryThreshold time.Duration
|
||||||
|
RenewalThreshold time.Duration
|
||||||
|
UrgentThreshold time.Duration
|
||||||
|
MaxRetries int
|
||||||
|
RetryInterval time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
type RenewalScheduler struct {
|
type RenewalScheduler struct {
|
||||||
lister DomainLister
|
lister DomainLister
|
||||||
issuer CertificateIssuer
|
issuer CertificateIssuer
|
||||||
threshold time.Duration
|
policy RenewalPolicy
|
||||||
now func() time.Time
|
now func() time.Time
|
||||||
interval time.Duration
|
mu sync.Mutex
|
||||||
mu sync.Mutex
|
runMu sync.Mutex
|
||||||
runMu sync.Mutex
|
running bool
|
||||||
running bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type RenewalReport struct {
|
type RenewalReport struct {
|
||||||
Checked int `json:"checked"`
|
Checked int `json:"checked"`
|
||||||
Selected int `json:"selected"`
|
NearExpiry int `json:"near_expiry"`
|
||||||
Succeeded int `json:"succeeded"`
|
Selected int `json:"selected"`
|
||||||
Failed int `json:"failed"`
|
Urgent int `json:"urgent"`
|
||||||
Errors []string `json:"errors,omitempty"`
|
Succeeded int `json:"succeeded"`
|
||||||
|
Failed int `json:"failed"`
|
||||||
|
Errors []string `json:"errors,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRenewalScheduler(lister DomainLister, issuer CertificateIssuer, thresholdDays int) *RenewalScheduler {
|
func NewRenewalScheduler(lister DomainLister, issuer CertificateIssuer, thresholdDays int) *RenewalScheduler {
|
||||||
if thresholdDays < 1 {
|
if thresholdDays < 1 {
|
||||||
thresholdDays = 6
|
thresholdDays = 6
|
||||||
}
|
}
|
||||||
return &RenewalScheduler{
|
return NewRenewalSchedulerWithPolicy(lister, issuer, RenewalPolicy{
|
||||||
lister: lister, issuer: issuer, threshold: time.Duration(thresholdDays) * 24 * time.Hour,
|
CheckInterval: 3 * 24 * time.Hour,
|
||||||
now: time.Now, interval: 24 * time.Hour,
|
NearExpiryThreshold: 10 * 24 * time.Hour,
|
||||||
}
|
RenewalThreshold: time.Duration(thresholdDays) * 24 * time.Hour,
|
||||||
|
UrgentThreshold: 3 * 24 * time.Hour,
|
||||||
|
MaxRetries: 3,
|
||||||
|
RetryInterval: 30 * time.Minute,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRenewalSchedulerWithPolicy(lister DomainLister, issuer CertificateIssuer, policy RenewalPolicy) *RenewalScheduler {
|
||||||
|
policy = normalizeRenewalPolicy(policy)
|
||||||
|
return &RenewalScheduler{lister: lister, issuer: issuer, policy: policy, now: time.Now}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *RenewalScheduler) RunOnce(ctx context.Context) (RenewalReport, error) {
|
func (s *RenewalScheduler) RunOnce(ctx context.Context) (RenewalReport, error) {
|
||||||
@ -65,11 +84,19 @@ func (s *RenewalScheduler) RunOnce(ctx context.Context) (RenewalReport, error) {
|
|||||||
if !item.AutoRenew || item.Status == "issuing" {
|
if !item.AutoRenew || item.Status == "issuing" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if item.ExpiresAt != nil && item.ExpiresAt.Sub(now) > s.threshold {
|
remaining := renewalRemaining(item.ExpiresAt, now)
|
||||||
|
if item.ExpiresAt != nil && remaining <= s.policy.NearExpiryThreshold {
|
||||||
|
report.NearExpiry++
|
||||||
|
}
|
||||||
|
if item.ExpiresAt != nil && remaining > s.policy.RenewalThreshold {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
report.Selected++
|
report.Selected++
|
||||||
if _, err := s.issuer.Issue(ctx, item.ID); err != nil {
|
urgent := item.ExpiresAt != nil && remaining <= s.policy.UrgentThreshold
|
||||||
|
if urgent {
|
||||||
|
report.Urgent++
|
||||||
|
}
|
||||||
|
if err := s.issueWithRetry(ctx, item.ID, urgent); 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
|
||||||
@ -96,16 +123,84 @@ func (s *RenewalScheduler) Start(ctx context.Context) {
|
|||||||
s.running = false
|
s.running = false
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
}()
|
}()
|
||||||
_, _ = s.RunOnce(ctx)
|
report, err := s.RunOnce(ctx)
|
||||||
ticker := time.NewTicker(s.interval)
|
|
||||||
defer ticker.Stop()
|
|
||||||
for {
|
for {
|
||||||
|
interval := s.policy.CheckInterval
|
||||||
|
if err == nil && report.NearExpiry > 0 {
|
||||||
|
interval = 24 * time.Hour
|
||||||
|
}
|
||||||
|
timer := time.NewTimer(interval)
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
if !timer.Stop() {
|
||||||
|
<-timer.C
|
||||||
|
}
|
||||||
return
|
return
|
||||||
case <-ticker.C:
|
case <-timer.C:
|
||||||
_, _ = s.RunOnce(ctx)
|
|
||||||
}
|
}
|
||||||
|
report, err = s.RunOnce(ctx)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *RenewalScheduler) issueWithRetry(ctx context.Context, domainID string, urgent bool) error {
|
||||||
|
attempts := 1
|
||||||
|
if urgent {
|
||||||
|
attempts = s.policy.MaxRetries
|
||||||
|
}
|
||||||
|
var lastErr error
|
||||||
|
for attempt := 1; attempt <= attempts; attempt++ {
|
||||||
|
if _, err := s.issuer.Issue(ctx, domainID); err == nil {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
lastErr = err
|
||||||
|
}
|
||||||
|
if attempt == attempts {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err := waitForRenewalRetry(ctx, s.policy.RetryInterval); err != nil {
|
||||||
|
return fmt.Errorf("第 %d 次续期失败,等待重试被取消:%w", attempt, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("续期失败(已尝试 %d 次):%w", attempts, lastErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForRenewalRetry(ctx context.Context, interval time.Duration) error {
|
||||||
|
timer := time.NewTimer(interval)
|
||||||
|
defer timer.Stop()
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case <-timer.C:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func renewalRemaining(expiresAt *time.Time, now time.Time) time.Duration {
|
||||||
|
if expiresAt == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return expiresAt.Sub(now)
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeRenewalPolicy(policy RenewalPolicy) RenewalPolicy {
|
||||||
|
if policy.CheckInterval <= 0 {
|
||||||
|
policy.CheckInterval = 3 * 24 * time.Hour
|
||||||
|
}
|
||||||
|
if policy.NearExpiryThreshold <= 0 {
|
||||||
|
policy.NearExpiryThreshold = 10 * 24 * time.Hour
|
||||||
|
}
|
||||||
|
if policy.RenewalThreshold <= 0 {
|
||||||
|
policy.RenewalThreshold = 6 * 24 * time.Hour
|
||||||
|
}
|
||||||
|
if policy.UrgentThreshold <= 0 {
|
||||||
|
policy.UrgentThreshold = 3 * 24 * time.Hour
|
||||||
|
}
|
||||||
|
if policy.MaxRetries <= 0 {
|
||||||
|
policy.MaxRetries = 3
|
||||||
|
}
|
||||||
|
if policy.RetryInterval <= 0 {
|
||||||
|
policy.RetryInterval = 30 * time.Minute
|
||||||
|
}
|
||||||
|
return policy
|
||||||
|
}
|
||||||
|
|||||||
@ -53,7 +53,7 @@ func TestRenewalSchedulerSelectsOnlyExpiringAutoRenewDomains(t *testing.T) {
|
|||||||
|
|
||||||
func TestRenewalSchedulerReportsFailures(t *testing.T) {
|
func TestRenewalSchedulerReportsFailures(t *testing.T) {
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
expires := now.Add(time.Hour)
|
expires := now.Add(4 * 24 * time.Hour)
|
||||||
item := domain.Domain{ID: "one", Name: "one.example.com", AutoRenew: true, ExpiresAt: &expires}
|
item := domain.Domain{ID: "one", Name: "one.example.com", AutoRenew: true, ExpiresAt: &expires}
|
||||||
scheduler := NewRenewalScheduler(renewalLister{items: []domain.Domain{item}}, &renewalService{err: errors.New("模拟续期失败")}, 6)
|
scheduler := NewRenewalScheduler(renewalLister{items: []domain.Domain{item}}, &renewalService{err: errors.New("模拟续期失败")}, 6)
|
||||||
report, err := scheduler.RunOnce(context.Background())
|
report, err := scheduler.RunOnce(context.Background())
|
||||||
@ -64,3 +64,54 @@ func TestRenewalSchedulerReportsFailures(t *testing.T) {
|
|||||||
t.Fatalf("续期失败结果错误:%+v", report)
|
t.Fatalf("续期失败结果错误:%+v", report)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRenewalSchedulerRetriesUrgentRenewal(t *testing.T) {
|
||||||
|
now := time.Date(2026, 8, 26, 0, 0, 0, 0, time.UTC)
|
||||||
|
expires := now.Add(2 * 24 * time.Hour)
|
||||||
|
service := &renewalService{err: errors.New("模拟紧急续期失败")}
|
||||||
|
scheduler := NewRenewalSchedulerWithPolicy(renewalLister{items: []domain.Domain{{
|
||||||
|
ID: "urgent", Name: "urgent.example.com", AutoRenew: true, ExpiresAt: &expires,
|
||||||
|
}}}, service, RenewalPolicy{
|
||||||
|
CheckInterval: time.Hour,
|
||||||
|
NearExpiryThreshold: 10 * 24 * time.Hour,
|
||||||
|
RenewalThreshold: 6 * 24 * time.Hour,
|
||||||
|
UrgentThreshold: 3 * 24 * time.Hour,
|
||||||
|
MaxRetries: 3,
|
||||||
|
RetryInterval: time.Nanosecond,
|
||||||
|
})
|
||||||
|
scheduler.now = func() time.Time { return now }
|
||||||
|
|
||||||
|
report, err := scheduler.RunOnce(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if report.Selected != 1 || report.Urgent != 1 || report.Failed != 1 || report.Succeeded != 0 {
|
||||||
|
t.Fatalf("紧急续期报告错误:%+v", report)
|
||||||
|
}
|
||||||
|
if len(service.ids) != 3 {
|
||||||
|
t.Fatalf("紧急续期应按最大重试次数执行:%v", service.ids)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRenewalSchedulerStopsRetryWhenContextCanceled(t *testing.T) {
|
||||||
|
now := time.Now().UTC()
|
||||||
|
expires := now.Add(2 * 24 * time.Hour)
|
||||||
|
service := &renewalService{err: errors.New("模拟紧急续期失败")}
|
||||||
|
scheduler := NewRenewalSchedulerWithPolicy(renewalLister{items: []domain.Domain{{
|
||||||
|
ID: "urgent", Name: "urgent.example.com", AutoRenew: true, ExpiresAt: &expires,
|
||||||
|
}}}, service, RenewalPolicy{MaxRetries: 3, RetryInterval: time.Hour})
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
time.Sleep(5 * time.Millisecond)
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
report, err := scheduler.RunOnce(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if report.Failed != 1 || len(service.ids) != 1 {
|
||||||
|
t.Fatalf("取消后的紧急续期重试报告错误:%+v calls=%d", report, len(service.ids))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -77,7 +77,12 @@ type ACMEConfig struct {
|
|||||||
CA string `json:"ca"`
|
CA string `json:"ca"`
|
||||||
Environment string `json:"environment"`
|
Environment string `json:"environment"`
|
||||||
AccountEmail string `json:"account_email"`
|
AccountEmail string `json:"account_email"`
|
||||||
|
RenewalCheckIntervalDays int `json:"renewal_check_interval_days"`
|
||||||
|
RenewalNearExpiryDays int `json:"renewal_near_expiry_days"`
|
||||||
RenewalThresholdDays int `json:"renewal_threshold_days"`
|
RenewalThresholdDays int `json:"renewal_threshold_days"`
|
||||||
|
RenewalUrgentThresholdDays int `json:"renewal_urgent_threshold_days"`
|
||||||
|
RenewalMaxRetries int `json:"renewal_max_retries"`
|
||||||
|
RenewalRetryIntervalMins int `json:"renewal_retry_interval_mins"`
|
||||||
PropagationTimeoutSecs int `json:"propagation_timeout_secs"`
|
PropagationTimeoutSecs int `json:"propagation_timeout_secs"`
|
||||||
PropagationPollingIntervalSecs int `json:"propagation_polling_interval_secs"`
|
PropagationPollingIntervalSecs int `json:"propagation_polling_interval_secs"`
|
||||||
}
|
}
|
||||||
@ -132,7 +137,12 @@ func Default(baseDir string) Config {
|
|||||||
ACME: ACMEConfig{
|
ACME: ACMEConfig{
|
||||||
CA: "letsencrypt",
|
CA: "letsencrypt",
|
||||||
Environment: "staging",
|
Environment: "staging",
|
||||||
|
RenewalCheckIntervalDays: 3,
|
||||||
|
RenewalNearExpiryDays: 10,
|
||||||
RenewalThresholdDays: 6,
|
RenewalThresholdDays: 6,
|
||||||
|
RenewalUrgentThresholdDays: 3,
|
||||||
|
RenewalMaxRetries: 3,
|
||||||
|
RenewalRetryIntervalMins: 30,
|
||||||
PropagationTimeoutSecs: 900,
|
PropagationTimeoutSecs: 900,
|
||||||
PropagationPollingIntervalSecs: 10,
|
PropagationPollingIntervalSecs: 10,
|
||||||
},
|
},
|
||||||
@ -258,7 +268,12 @@ func (c *Config) ApplyEnvironment() error {
|
|||||||
target *int
|
target *int
|
||||||
}{
|
}{
|
||||||
{"SCERT_SERVER_PORT", &c.Server.Port},
|
{"SCERT_SERVER_PORT", &c.Server.Port},
|
||||||
|
{"SCERT_ACME_RENEWAL_CHECK_INTERVAL_DAYS", &c.ACME.RenewalCheckIntervalDays},
|
||||||
|
{"SCERT_ACME_RENEWAL_NEAR_EXPIRY_DAYS", &c.ACME.RenewalNearExpiryDays},
|
||||||
{"SCERT_ACME_RENEWAL_THRESHOLD_DAYS", &c.ACME.RenewalThresholdDays},
|
{"SCERT_ACME_RENEWAL_THRESHOLD_DAYS", &c.ACME.RenewalThresholdDays},
|
||||||
|
{"SCERT_ACME_RENEWAL_URGENT_THRESHOLD_DAYS", &c.ACME.RenewalUrgentThresholdDays},
|
||||||
|
{"SCERT_ACME_RENEWAL_MAX_RETRIES", &c.ACME.RenewalMaxRetries},
|
||||||
|
{"SCERT_ACME_RENEWAL_RETRY_INTERVAL_MINS", &c.ACME.RenewalRetryIntervalMins},
|
||||||
{"SCERT_ACME_PROPAGATION_TIMEOUT_SECS", &c.ACME.PropagationTimeoutSecs},
|
{"SCERT_ACME_PROPAGATION_TIMEOUT_SECS", &c.ACME.PropagationTimeoutSecs},
|
||||||
{"SCERT_ACME_PROPAGATION_POLLING_INTERVAL_SECS", &c.ACME.PropagationPollingIntervalSecs},
|
{"SCERT_ACME_PROPAGATION_POLLING_INTERVAL_SECS", &c.ACME.PropagationPollingIntervalSecs},
|
||||||
}
|
}
|
||||||
@ -295,6 +310,24 @@ func (c *Config) Normalize() {
|
|||||||
if c.ACME.PropagationTimeoutSecs == 0 {
|
if c.ACME.PropagationTimeoutSecs == 0 {
|
||||||
c.ACME.PropagationTimeoutSecs = 900
|
c.ACME.PropagationTimeoutSecs = 900
|
||||||
}
|
}
|
||||||
|
if c.ACME.RenewalCheckIntervalDays == 0 {
|
||||||
|
c.ACME.RenewalCheckIntervalDays = 3
|
||||||
|
}
|
||||||
|
if c.ACME.RenewalNearExpiryDays == 0 {
|
||||||
|
c.ACME.RenewalNearExpiryDays = 10
|
||||||
|
}
|
||||||
|
if c.ACME.RenewalThresholdDays == 0 {
|
||||||
|
c.ACME.RenewalThresholdDays = 6
|
||||||
|
}
|
||||||
|
if c.ACME.RenewalUrgentThresholdDays == 0 {
|
||||||
|
c.ACME.RenewalUrgentThresholdDays = 3
|
||||||
|
}
|
||||||
|
if c.ACME.RenewalMaxRetries == 0 {
|
||||||
|
c.ACME.RenewalMaxRetries = 3
|
||||||
|
}
|
||||||
|
if c.ACME.RenewalRetryIntervalMins == 0 {
|
||||||
|
c.ACME.RenewalRetryIntervalMins = 30
|
||||||
|
}
|
||||||
if c.ACME.PropagationPollingIntervalSecs == 0 {
|
if c.ACME.PropagationPollingIntervalSecs == 0 {
|
||||||
c.ACME.PropagationPollingIntervalSecs = 10
|
c.ACME.PropagationPollingIntervalSecs = 10
|
||||||
}
|
}
|
||||||
@ -435,6 +468,21 @@ func (c Config) Validate() error {
|
|||||||
if c.ACME.RenewalThresholdDays < 1 {
|
if c.ACME.RenewalThresholdDays < 1 {
|
||||||
return fmt.Errorf("续期阈值必须大于 0")
|
return fmt.Errorf("续期阈值必须大于 0")
|
||||||
}
|
}
|
||||||
|
if c.ACME.RenewalCheckIntervalDays < 1 || c.ACME.RenewalNearExpiryDays < 1 || c.ACME.RenewalUrgentThresholdDays < 1 {
|
||||||
|
return fmt.Errorf("续期检查阈值必须大于 0")
|
||||||
|
}
|
||||||
|
if c.ACME.RenewalUrgentThresholdDays > c.ACME.RenewalThresholdDays {
|
||||||
|
return fmt.Errorf("紧急续期阈值不能大于自动续期阈值")
|
||||||
|
}
|
||||||
|
if c.ACME.RenewalThresholdDays > c.ACME.RenewalNearExpiryDays {
|
||||||
|
return fmt.Errorf("自动续期阈值不能大于临近到期检查阈值")
|
||||||
|
}
|
||||||
|
if c.ACME.RenewalMaxRetries < 1 || c.ACME.RenewalMaxRetries > 10 {
|
||||||
|
return fmt.Errorf("续期最大重试次数必须在 1 到 10 次之间")
|
||||||
|
}
|
||||||
|
if c.ACME.RenewalRetryIntervalMins < 1 || c.ACME.RenewalRetryIntervalMins > 1440 {
|
||||||
|
return fmt.Errorf("续期重试间隔必须在 1 到 1440 分钟之间")
|
||||||
|
}
|
||||||
if c.ACME.PropagationTimeoutSecs < 1 || c.ACME.PropagationTimeoutSecs > 86400 {
|
if c.ACME.PropagationTimeoutSecs < 1 || c.ACME.PropagationTimeoutSecs > 86400 {
|
||||||
return fmt.Errorf("DNS 传播等待时间必须在 1 到 86400 秒之间")
|
return fmt.Errorf("DNS 传播等待时间必须在 1 到 86400 秒之间")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -161,6 +161,52 @@ func TestDefaultACMEPropagationSettings(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDefaultRenewalPolicy(t *testing.T) {
|
||||||
|
cfg := Default(t.TempDir())
|
||||||
|
if cfg.ACME.RenewalCheckIntervalDays != 3 || cfg.ACME.RenewalNearExpiryDays != 10 ||
|
||||||
|
cfg.ACME.RenewalThresholdDays != 6 || cfg.ACME.RenewalUrgentThresholdDays != 3 ||
|
||||||
|
cfg.ACME.RenewalMaxRetries != 3 || cfg.ACME.RenewalRetryIntervalMins != 30 {
|
||||||
|
t.Fatalf("默认续期策略不正确:%+v", cfg.ACME)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeRestoresMissingRenewalPolicy(t *testing.T) {
|
||||||
|
cfg := Default(t.TempDir())
|
||||||
|
cfg.ACME.RenewalCheckIntervalDays = 0
|
||||||
|
cfg.ACME.RenewalNearExpiryDays = 0
|
||||||
|
cfg.ACME.RenewalThresholdDays = 0
|
||||||
|
cfg.ACME.RenewalUrgentThresholdDays = 0
|
||||||
|
cfg.ACME.RenewalMaxRetries = 0
|
||||||
|
cfg.ACME.RenewalRetryIntervalMins = 0
|
||||||
|
cfg.Normalize()
|
||||||
|
if cfg.ACME.RenewalCheckIntervalDays != 3 || cfg.ACME.RenewalNearExpiryDays != 10 ||
|
||||||
|
cfg.ACME.RenewalThresholdDays != 6 || cfg.ACME.RenewalUrgentThresholdDays != 3 ||
|
||||||
|
cfg.ACME.RenewalMaxRetries != 3 || cfg.ACME.RenewalRetryIntervalMins != 30 {
|
||||||
|
t.Fatalf("旧配置的续期策略未补齐:%+v", cfg.ACME)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateRejectsInvalidRenewalPolicy(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
edit func(*Config)
|
||||||
|
}{
|
||||||
|
{name: "urgent threshold", edit: func(cfg *Config) { cfg.ACME.RenewalUrgentThresholdDays = 7 }},
|
||||||
|
{name: "renewal threshold", edit: func(cfg *Config) { cfg.ACME.RenewalThresholdDays = 11 }},
|
||||||
|
{name: "retry count", edit: func(cfg *Config) { cfg.ACME.RenewalMaxRetries = 0 }},
|
||||||
|
{name: "retry interval", edit: func(cfg *Config) { cfg.ACME.RenewalRetryIntervalMins = 0 }},
|
||||||
|
}
|
||||||
|
for _, item := range cases {
|
||||||
|
t.Run(item.name, func(t *testing.T) {
|
||||||
|
cfg := Default(t.TempDir())
|
||||||
|
item.edit(&cfg)
|
||||||
|
if err := cfg.Validate(); err == nil {
|
||||||
|
t.Fatal("无效续期策略未被拒绝")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestNormalizeRestoresMissingACMEPropagationSettings(t *testing.T) {
|
func TestNormalizeRestoresMissingACMEPropagationSettings(t *testing.T) {
|
||||||
cfg := Default(t.TempDir())
|
cfg := Default(t.TempDir())
|
||||||
cfg.ACME.PropagationTimeoutSecs = 0
|
cfg.ACME.PropagationTimeoutSecs = 0
|
||||||
|
|||||||
@ -40,7 +40,12 @@ type settingsACMEView struct {
|
|||||||
CA string `json:"ca"`
|
CA string `json:"ca"`
|
||||||
Environment string `json:"environment"`
|
Environment string `json:"environment"`
|
||||||
AccountEmail string `json:"account_email"`
|
AccountEmail string `json:"account_email"`
|
||||||
|
RenewalCheckIntervalDays int `json:"renewal_check_interval_days"`
|
||||||
|
RenewalNearExpiryDays int `json:"renewal_near_expiry_days"`
|
||||||
RenewalThresholdDays int `json:"renewal_threshold_days"`
|
RenewalThresholdDays int `json:"renewal_threshold_days"`
|
||||||
|
RenewalUrgentThresholdDays int `json:"renewal_urgent_threshold_days"`
|
||||||
|
RenewalMaxRetries int `json:"renewal_max_retries"`
|
||||||
|
RenewalRetryIntervalMins int `json:"renewal_retry_interval_mins"`
|
||||||
PropagationTimeoutSecs int `json:"propagation_timeout_secs"`
|
PropagationTimeoutSecs int `json:"propagation_timeout_secs"`
|
||||||
PropagationPollingIntervalSecs int `json:"propagation_polling_interval_secs"`
|
PropagationPollingIntervalSecs int `json:"propagation_polling_interval_secs"`
|
||||||
}
|
}
|
||||||
@ -106,7 +111,12 @@ type settingsACMERequest struct {
|
|||||||
CA *string `json:"ca"`
|
CA *string `json:"ca"`
|
||||||
Environment *string `json:"environment"`
|
Environment *string `json:"environment"`
|
||||||
AccountEmail *string `json:"account_email"`
|
AccountEmail *string `json:"account_email"`
|
||||||
|
RenewalCheckIntervalDays *int `json:"renewal_check_interval_days"`
|
||||||
|
RenewalNearExpiryDays *int `json:"renewal_near_expiry_days"`
|
||||||
RenewalThresholdDays *int `json:"renewal_threshold_days"`
|
RenewalThresholdDays *int `json:"renewal_threshold_days"`
|
||||||
|
RenewalUrgentThresholdDays *int `json:"renewal_urgent_threshold_days"`
|
||||||
|
RenewalMaxRetries *int `json:"renewal_max_retries"`
|
||||||
|
RenewalRetryIntervalMins *int `json:"renewal_retry_interval_mins"`
|
||||||
PropagationTimeoutSecs *int `json:"propagation_timeout_secs"`
|
PropagationTimeoutSecs *int `json:"propagation_timeout_secs"`
|
||||||
PropagationPollingIntervalSecs *int `json:"propagation_polling_interval_secs"`
|
PropagationPollingIntervalSecs *int `json:"propagation_polling_interval_secs"`
|
||||||
}
|
}
|
||||||
@ -233,8 +243,14 @@ func settingsViewFromConfig(cfg config.Config) 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},
|
||||||
Web: settingsWebView{Enabled: cfg.Web.Enabled, Hostname: cfg.Web.Hostname, PublicIP: cfg.Web.PublicIP},
|
Web: settingsWebView{Enabled: cfg.Web.Enabled, Hostname: cfg.Web.Hostname, PublicIP: cfg.Web.PublicIP},
|
||||||
Paths: settingsPathsView{Certificate: cfg.Paths.Certificate},
|
Paths: settingsPathsView{Certificate: cfg.Paths.Certificate},
|
||||||
ACME: settingsACMEView{CA: cfg.ACME.CA, Environment: cfg.ACME.Environment, AccountEmail: cfg.ACME.AccountEmail, RenewalThresholdDays: cfg.ACME.RenewalThresholdDays, PropagationTimeoutSecs: cfg.ACME.PropagationTimeoutSecs, PropagationPollingIntervalSecs: cfg.ACME.PropagationPollingIntervalSecs},
|
ACME: settingsACMEView{
|
||||||
DNS: settingsDNSView{Provider: cfg.DNS.Provider, VerificationState: cfg.DNS.VerificationState},
|
CA: cfg.ACME.CA, Environment: cfg.ACME.Environment, AccountEmail: cfg.ACME.AccountEmail,
|
||||||
|
RenewalCheckIntervalDays: cfg.ACME.RenewalCheckIntervalDays, RenewalNearExpiryDays: cfg.ACME.RenewalNearExpiryDays,
|
||||||
|
RenewalThresholdDays: cfg.ACME.RenewalThresholdDays, RenewalUrgentThresholdDays: cfg.ACME.RenewalUrgentThresholdDays,
|
||||||
|
RenewalMaxRetries: cfg.ACME.RenewalMaxRetries, RenewalRetryIntervalMins: cfg.ACME.RenewalRetryIntervalMins,
|
||||||
|
PropagationTimeoutSecs: cfg.ACME.PropagationTimeoutSecs, PropagationPollingIntervalSecs: cfg.ACME.PropagationPollingIntervalSecs,
|
||||||
|
},
|
||||||
|
DNS: settingsDNSView{Provider: cfg.DNS.Provider, VerificationState: cfg.DNS.VerificationState},
|
||||||
Notification: settingsNotificationView{
|
Notification: settingsNotificationView{
|
||||||
HTTPEnabled: cfg.Notification.HTTPEnabled, SMTPEnabled: cfg.Notification.SMTPEnabled, Provider: cfg.Notification.Provider,
|
HTTPEnabled: cfg.Notification.HTTPEnabled, SMTPEnabled: cfg.Notification.SMTPEnabled, Provider: cfg.Notification.Provider,
|
||||||
SMTPHost: cfg.Notification.SMTPHost, SMTPPort: cfg.Notification.SMTPPort, SMTPUsername: cfg.Notification.SMTPUsername,
|
SMTPHost: cfg.Notification.SMTPHost, SMTPPort: cfg.Notification.SMTPPort, SMTPUsername: cfg.Notification.SMTPUsername,
|
||||||
@ -293,6 +309,21 @@ func applySettingsRequest(cfg *config.Config, request settingsRequest) {
|
|||||||
if request.ACME.RenewalThresholdDays != nil {
|
if request.ACME.RenewalThresholdDays != nil {
|
||||||
cfg.ACME.RenewalThresholdDays = *request.ACME.RenewalThresholdDays
|
cfg.ACME.RenewalThresholdDays = *request.ACME.RenewalThresholdDays
|
||||||
}
|
}
|
||||||
|
if request.ACME.RenewalCheckIntervalDays != nil {
|
||||||
|
cfg.ACME.RenewalCheckIntervalDays = *request.ACME.RenewalCheckIntervalDays
|
||||||
|
}
|
||||||
|
if request.ACME.RenewalNearExpiryDays != nil {
|
||||||
|
cfg.ACME.RenewalNearExpiryDays = *request.ACME.RenewalNearExpiryDays
|
||||||
|
}
|
||||||
|
if request.ACME.RenewalUrgentThresholdDays != nil {
|
||||||
|
cfg.ACME.RenewalUrgentThresholdDays = *request.ACME.RenewalUrgentThresholdDays
|
||||||
|
}
|
||||||
|
if request.ACME.RenewalMaxRetries != nil {
|
||||||
|
cfg.ACME.RenewalMaxRetries = *request.ACME.RenewalMaxRetries
|
||||||
|
}
|
||||||
|
if request.ACME.RenewalRetryIntervalMins != nil {
|
||||||
|
cfg.ACME.RenewalRetryIntervalMins = *request.ACME.RenewalRetryIntervalMins
|
||||||
|
}
|
||||||
if request.ACME.PropagationTimeoutSecs != nil {
|
if request.ACME.PropagationTimeoutSecs != nil {
|
||||||
cfg.ACME.PropagationTimeoutSecs = *request.ACME.PropagationTimeoutSecs
|
cfg.ACME.PropagationTimeoutSecs = *request.ACME.PropagationTimeoutSecs
|
||||||
}
|
}
|
||||||
|
|||||||
@ -224,7 +224,12 @@ export function useDashboard() {
|
|||||||
ca: "letsencrypt",
|
ca: "letsencrypt",
|
||||||
environment: "staging",
|
environment: "staging",
|
||||||
account_email: "",
|
account_email: "",
|
||||||
|
renewal_check_interval_days: 3,
|
||||||
|
renewal_near_expiry_days: 10,
|
||||||
renewal_threshold_days: 6,
|
renewal_threshold_days: 6,
|
||||||
|
renewal_urgent_threshold_days: 3,
|
||||||
|
renewal_max_retries: 3,
|
||||||
|
renewal_retry_interval_mins: 30,
|
||||||
propagation_timeout_secs: 900,
|
propagation_timeout_secs: 900,
|
||||||
propagation_polling_interval_secs: 10,
|
propagation_polling_interval_secs: 10,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -423,6 +423,18 @@ const {
|
|||||||
<label class="field"><span>ACME 环境</span><select v-model="settingsForm.acme.environment"><option value="staging">测试环境</option><option value="production">生产环境</option></select></label>
|
<label class="field"><span>ACME 环境</span><select v-model="settingsForm.acme.environment"><option value="staging">测试环境</option><option value="production">生产环境</option></select></label>
|
||||||
<label class="field"><span>自动续期阈值(天)</span><input v-model.number="settingsForm.acme.renewal_threshold_days" type="number" min="1" max="365" /></label>
|
<label class="field"><span>自动续期阈值(天)</span><input v-model.number="settingsForm.acme.renewal_threshold_days" type="number" min="1" max="365" /></label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-grid">
|
||||||
|
<label class="field"><span>常规检查间隔(天)</span><input v-model.number="settingsForm.acme.renewal_check_interval_days" type="number" min="1" max="30" /></label>
|
||||||
|
<label class="field"><span>临近到期检查阈值(天)</span><input v-model.number="settingsForm.acme.renewal_near_expiry_days" type="number" min="1" max="365" /></label>
|
||||||
|
</div>
|
||||||
|
<div class="form-grid">
|
||||||
|
<label class="field"><span>紧急续期阈值(天)</span><input v-model.number="settingsForm.acme.renewal_urgent_threshold_days" type="number" min="1" max="365" /></label>
|
||||||
|
<label class="field"><span>紧急续期最大尝试次数</span><input v-model.number="settingsForm.acme.renewal_max_retries" type="number" min="1" max="10" /></label>
|
||||||
|
</div>
|
||||||
|
<div class="form-grid">
|
||||||
|
<label class="field"><span>续期重试间隔(分钟)</span><input v-model.number="settingsForm.acme.renewal_retry_interval_mins" type="number" min="1" max="1440" /></label>
|
||||||
|
<div class="field-hint">剩余有效期低于自动续期阈值时触发续期;低于紧急阈值时按配置重试。</div>
|
||||||
|
</div>
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<label class="field"><span>DNS 传播等待(秒)</span><input v-model.number="settingsForm.acme.propagation_timeout_secs" type="number" min="1" /></label>
|
<label class="field"><span>DNS 传播等待(秒)</span><input v-model.number="settingsForm.acme.propagation_timeout_secs" type="number" min="1" /></label>
|
||||||
<label class="field"><span>DNS 检测间隔(秒)</span><input v-model.number="settingsForm.acme.propagation_polling_interval_secs" type="number" min="1" /></label>
|
<label class="field"><span>DNS 检测间隔(秒)</span><input v-model.number="settingsForm.acme.propagation_polling_interval_secs" type="number" min="1" /></label>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user