完善自动续期策略与管理配置
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) },
|
||||
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{
|
||||
Config: cfg, ConfigPath: configPath, Logger: logger, Repository: repo, Servers: serverRepo, Users: userRepo, SSHKeys: sshKeys,
|
||||
SetupDNS: dnsVerification, Secrets: secretStore, Firewall: firewall.NewManager(),
|
||||
|
||||
@ -18,33 +18,52 @@ type CertificateIssuer interface {
|
||||
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 {
|
||||
lister DomainLister
|
||||
issuer CertificateIssuer
|
||||
threshold time.Duration
|
||||
now func() time.Time
|
||||
interval time.Duration
|
||||
mu sync.Mutex
|
||||
runMu sync.Mutex
|
||||
running bool
|
||||
lister DomainLister
|
||||
issuer CertificateIssuer
|
||||
policy RenewalPolicy
|
||||
now func() time.Time
|
||||
mu sync.Mutex
|
||||
runMu sync.Mutex
|
||||
running bool
|
||||
}
|
||||
|
||||
type RenewalReport struct {
|
||||
Checked int `json:"checked"`
|
||||
Selected int `json:"selected"`
|
||||
Succeeded int `json:"succeeded"`
|
||||
Failed int `json:"failed"`
|
||||
Errors []string `json:"errors,omitempty"`
|
||||
Checked int `json:"checked"`
|
||||
NearExpiry int `json:"near_expiry"`
|
||||
Selected int `json:"selected"`
|
||||
Urgent int `json:"urgent"`
|
||||
Succeeded int `json:"succeeded"`
|
||||
Failed int `json:"failed"`
|
||||
Errors []string `json:"errors,omitempty"`
|
||||
}
|
||||
|
||||
func NewRenewalScheduler(lister DomainLister, issuer CertificateIssuer, thresholdDays int) *RenewalScheduler {
|
||||
if thresholdDays < 1 {
|
||||
thresholdDays = 6
|
||||
}
|
||||
return &RenewalScheduler{
|
||||
lister: lister, issuer: issuer, threshold: time.Duration(thresholdDays) * 24 * time.Hour,
|
||||
now: time.Now, interval: 24 * time.Hour,
|
||||
}
|
||||
return NewRenewalSchedulerWithPolicy(lister, issuer, RenewalPolicy{
|
||||
CheckInterval: 3 * 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) {
|
||||
@ -65,11 +84,19 @@ func (s *RenewalScheduler) RunOnce(ctx context.Context) (RenewalReport, error) {
|
||||
if !item.AutoRenew || item.Status == "issuing" {
|
||||
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
|
||||
}
|
||||
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.Errors = append(report.Errors, fmt.Sprintf("%s:%v", item.Name, err))
|
||||
continue
|
||||
@ -96,16 +123,84 @@ func (s *RenewalScheduler) Start(ctx context.Context) {
|
||||
s.running = false
|
||||
s.mu.Unlock()
|
||||
}()
|
||||
_, _ = s.RunOnce(ctx)
|
||||
ticker := time.NewTicker(s.interval)
|
||||
defer ticker.Stop()
|
||||
report, err := s.RunOnce(ctx)
|
||||
for {
|
||||
interval := s.policy.CheckInterval
|
||||
if err == nil && report.NearExpiry > 0 {
|
||||
interval = 24 * time.Hour
|
||||
}
|
||||
timer := time.NewTimer(interval)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if !timer.Stop() {
|
||||
<-timer.C
|
||||
}
|
||||
return
|
||||
case <-ticker.C:
|
||||
_, _ = s.RunOnce(ctx)
|
||||
case <-timer.C:
|
||||
}
|
||||
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) {
|
||||
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}
|
||||
scheduler := NewRenewalScheduler(renewalLister{items: []domain.Domain{item}}, &renewalService{err: errors.New("模拟续期失败")}, 6)
|
||||
report, err := scheduler.RunOnce(context.Background())
|
||||
@ -64,3 +64,54 @@ func TestRenewalSchedulerReportsFailures(t *testing.T) {
|
||||
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"`
|
||||
Environment string `json:"environment"`
|
||||
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"`
|
||||
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"`
|
||||
PropagationPollingIntervalSecs int `json:"propagation_polling_interval_secs"`
|
||||
}
|
||||
@ -132,7 +137,12 @@ func Default(baseDir string) Config {
|
||||
ACME: ACMEConfig{
|
||||
CA: "letsencrypt",
|
||||
Environment: "staging",
|
||||
RenewalCheckIntervalDays: 3,
|
||||
RenewalNearExpiryDays: 10,
|
||||
RenewalThresholdDays: 6,
|
||||
RenewalUrgentThresholdDays: 3,
|
||||
RenewalMaxRetries: 3,
|
||||
RenewalRetryIntervalMins: 30,
|
||||
PropagationTimeoutSecs: 900,
|
||||
PropagationPollingIntervalSecs: 10,
|
||||
},
|
||||
@ -258,7 +268,12 @@ func (c *Config) ApplyEnvironment() error {
|
||||
target *int
|
||||
}{
|
||||
{"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_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_POLLING_INTERVAL_SECS", &c.ACME.PropagationPollingIntervalSecs},
|
||||
}
|
||||
@ -295,6 +310,24 @@ func (c *Config) Normalize() {
|
||||
if c.ACME.PropagationTimeoutSecs == 0 {
|
||||
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 {
|
||||
c.ACME.PropagationPollingIntervalSecs = 10
|
||||
}
|
||||
@ -435,6 +468,21 @@ func (c Config) Validate() error {
|
||||
if c.ACME.RenewalThresholdDays < 1 {
|
||||
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 {
|
||||
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) {
|
||||
cfg := Default(t.TempDir())
|
||||
cfg.ACME.PropagationTimeoutSecs = 0
|
||||
|
||||
@ -40,7 +40,12 @@ type settingsACMEView struct {
|
||||
CA string `json:"ca"`
|
||||
Environment string `json:"environment"`
|
||||
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"`
|
||||
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"`
|
||||
PropagationPollingIntervalSecs int `json:"propagation_polling_interval_secs"`
|
||||
}
|
||||
@ -106,7 +111,12 @@ type settingsACMERequest struct {
|
||||
CA *string `json:"ca"`
|
||||
Environment *string `json:"environment"`
|
||||
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"`
|
||||
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"`
|
||||
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},
|
||||
Web: settingsWebView{Enabled: cfg.Web.Enabled, Hostname: cfg.Web.Hostname, PublicIP: cfg.Web.PublicIP},
|
||||
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},
|
||||
DNS: settingsDNSView{Provider: cfg.DNS.Provider, VerificationState: cfg.DNS.VerificationState},
|
||||
ACME: settingsACMEView{
|
||||
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{
|
||||
HTTPEnabled: cfg.Notification.HTTPEnabled, SMTPEnabled: cfg.Notification.SMTPEnabled, Provider: cfg.Notification.Provider,
|
||||
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 {
|
||||
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 {
|
||||
cfg.ACME.PropagationTimeoutSecs = *request.ACME.PropagationTimeoutSecs
|
||||
}
|
||||
|
||||
@ -224,7 +224,12 @@ export function useDashboard() {
|
||||
ca: "letsencrypt",
|
||||
environment: "staging",
|
||||
account_email: "",
|
||||
renewal_check_interval_days: 3,
|
||||
renewal_near_expiry_days: 10,
|
||||
renewal_threshold_days: 6,
|
||||
renewal_urgent_threshold_days: 3,
|
||||
renewal_max_retries: 3,
|
||||
renewal_retry_interval_mins: 30,
|
||||
propagation_timeout_secs: 900,
|
||||
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>自动续期阈值(天)</span><input v-model.number="settingsForm.acme.renewal_threshold_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_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">
|
||||
<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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user