mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-04 23:16:37 +08:00
migrate
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package tasks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
// DatabaseCleanupTargetAccessLogs is the API cleanup target for access logs.
|
||||
DatabaseCleanupTargetAccessLogs = "node_access_logs"
|
||||
// DatabaseCleanupTargetMetricSnapshots is the API cleanup target for metric snapshots.
|
||||
DatabaseCleanupTargetMetricSnapshots = "node_metric_snapshots"
|
||||
// DatabaseCleanupTargetRequestReports is the API cleanup target for request reports.
|
||||
DatabaseCleanupTargetRequestReports = "node_request_reports"
|
||||
)
|
||||
|
||||
var databaseCleanupTargets = map[string]string{
|
||||
DatabaseCleanupTargetAccessLogs: "访问日志",
|
||||
DatabaseCleanupTargetMetricSnapshots: "性能快照",
|
||||
DatabaseCleanupTargetRequestReports: "请求聚合",
|
||||
}
|
||||
|
||||
func init() {
|
||||
registerJob("database_auto_cleanup", "0 3 * * *", runDatabaseAutoCleanupJob)
|
||||
}
|
||||
|
||||
// DatabaseCleanupInput describes a manual observability cleanup request.
|
||||
type DatabaseCleanupInput struct {
|
||||
Target string `json:"target"`
|
||||
RetentionDays *int `json:"retention_days"`
|
||||
}
|
||||
|
||||
// DatabaseCleanupResult summarizes a manual observability cleanup run.
|
||||
type DatabaseCleanupResult struct {
|
||||
Target string `json:"target"`
|
||||
TargetLabel string `json:"target_label"`
|
||||
DeletedCount int64 `json:"deleted_count"`
|
||||
DeleteAll bool `json:"delete_all"`
|
||||
RetentionDays *int `json:"retention_days,omitempty"`
|
||||
Cutoff *time.Time `json:"cutoff,omitempty"`
|
||||
}
|
||||
|
||||
// DatabaseAutoCleanupSummary summarizes a scheduled auto-cleanup run.
|
||||
type DatabaseAutoCleanupSummary struct {
|
||||
RetentionDays int `json:"retention_days"`
|
||||
ExecutedAt time.Time `json:"executed_at"`
|
||||
Results []DatabaseCleanupResult `json:"results"`
|
||||
}
|
||||
|
||||
// CleanupDatabaseObservability deletes observability rows for the given target.
|
||||
func CleanupDatabaseObservability(ctx context.Context, input DatabaseCleanupInput) (*DatabaseCleanupResult, error) {
|
||||
target := strings.TrimSpace(input.Target)
|
||||
targetLabel, ok := databaseCleanupTargets[target]
|
||||
if !ok {
|
||||
return nil, errors.New("unsupported cleanup target")
|
||||
}
|
||||
if input.RetentionDays != nil && *input.RetentionDays <= 0 {
|
||||
return nil, errors.New("retention_days 必须为大于 0 的整数")
|
||||
}
|
||||
|
||||
result := &DatabaseCleanupResult{
|
||||
Target: target,
|
||||
TargetLabel: targetLabel,
|
||||
DeleteAll: input.RetentionDays == nil,
|
||||
}
|
||||
|
||||
if input.RetentionDays == nil {
|
||||
deleted, err := deleteAllObservabilityRows(ctx, target)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.DeletedCount = deleted
|
||||
return result, nil
|
||||
}
|
||||
|
||||
retentionDays := *input.RetentionDays
|
||||
cutoff := time.Now().UTC().Add(-time.Duration(retentionDays) * 24 * time.Hour)
|
||||
deleted, err := deleteObservabilityRowsBefore(ctx, target, cutoff)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.DeletedCount = deleted
|
||||
result.RetentionDays = &retentionDays
|
||||
result.Cutoff = &cutoff
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// RunDatabaseAutoCleanupOnce runs retention-based cleanup for all observability targets.
|
||||
func RunDatabaseAutoCleanupOnce(now time.Time) (*DatabaseAutoCleanupSummary, error) {
|
||||
if !model.DatabaseAutoCleanupEnabled {
|
||||
return nil, nil
|
||||
}
|
||||
if model.DatabaseAutoCleanupRetentionDays < 1 {
|
||||
return nil, fmt.Errorf("database auto cleanup retention_days must be at least 1")
|
||||
}
|
||||
|
||||
retentionDays := model.DatabaseAutoCleanupRetentionDays
|
||||
ctx := context.Background()
|
||||
results := make([]DatabaseCleanupResult, 0, len(databaseCleanupTargets))
|
||||
for _, target := range []string{
|
||||
DatabaseCleanupTargetAccessLogs,
|
||||
DatabaseCleanupTargetMetricSnapshots,
|
||||
DatabaseCleanupTargetRequestReports,
|
||||
} {
|
||||
result, err := CleanupDatabaseObservability(ctx, DatabaseCleanupInput{
|
||||
Target: target,
|
||||
RetentionDays: &retentionDays,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results = append(results, *result)
|
||||
}
|
||||
|
||||
return &DatabaseAutoCleanupSummary{
|
||||
RetentionDays: retentionDays,
|
||||
ExecutedAt: now.UTC(),
|
||||
Results: results,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func runDatabaseAutoCleanupJob(ctx context.Context) {
|
||||
summary, err := RunDatabaseAutoCleanupOnce(time.Now())
|
||||
if err != nil {
|
||||
logger.ErrorF(ctx, "[OpenFlareTasks] database auto cleanup failed: %v", err)
|
||||
return
|
||||
}
|
||||
if summary == nil {
|
||||
return
|
||||
}
|
||||
|
||||
totalDeleted := int64(0)
|
||||
for _, item := range summary.Results {
|
||||
totalDeleted += item.DeletedCount
|
||||
}
|
||||
logger.InfoF(
|
||||
ctx,
|
||||
"[OpenFlareTasks] database auto cleanup completed retention_days=%d deleted_count=%d",
|
||||
summary.RetentionDays,
|
||||
totalDeleted,
|
||||
)
|
||||
}
|
||||
|
||||
func deleteAllObservabilityRows(ctx context.Context, target string) (int64, error) {
|
||||
switch target {
|
||||
case DatabaseCleanupTargetAccessLogs:
|
||||
return model.DeleteAllOpenFlareAccessLogs(ctx)
|
||||
case DatabaseCleanupTargetMetricSnapshots:
|
||||
return model.DeleteAllOpenFlareMetricSnapshots(ctx)
|
||||
case DatabaseCleanupTargetRequestReports:
|
||||
return model.DeleteAllOpenFlareRequestReports(ctx)
|
||||
default:
|
||||
return 0, errors.New("unsupported cleanup target")
|
||||
}
|
||||
}
|
||||
|
||||
func deleteObservabilityRowsBefore(ctx context.Context, target string, cutoff time.Time) (int64, error) {
|
||||
switch target {
|
||||
case DatabaseCleanupTargetAccessLogs:
|
||||
return model.DeleteOpenFlareAccessLogsBefore(ctx, cutoff)
|
||||
case DatabaseCleanupTargetMetricSnapshots:
|
||||
return model.DeleteOpenFlareMetricSnapshotsBefore(ctx, cutoff)
|
||||
case DatabaseCleanupTargetRequestReports:
|
||||
return model.DeleteOpenFlareRequestReportsBefore(ctx, cutoff)
|
||||
default:
|
||||
return 0, errors.New("unsupported cleanup target")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package tasks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func setupDatabaseCleanupTestDB(t *testing.T) context.Context {
|
||||
t.Helper()
|
||||
|
||||
sqliteDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
|
||||
DisableForeignKeyConstraintWhenMigrating: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, sqliteDB.AutoMigrate(
|
||||
&model.OpenFlareAccessLog{},
|
||||
&model.OpenFlareMetricSnapshot{},
|
||||
&model.OpenFlareRequestReport{},
|
||||
))
|
||||
db.SetDB(sqliteDB)
|
||||
t.Cleanup(func() {
|
||||
db.SetDB(nil)
|
||||
})
|
||||
return context.Background()
|
||||
}
|
||||
|
||||
func TestCleanupDatabaseObservabilityDeletesTargetedRows(t *testing.T) {
|
||||
ctx := setupDatabaseCleanupTestDB(t)
|
||||
now := time.Now().UTC()
|
||||
|
||||
require.NoError(t, db.DB(ctx).Create(&model.OpenFlareMetricSnapshot{
|
||||
NodeID: "node-a",
|
||||
CapturedAt: now.Add(-10 * 24 * time.Hour),
|
||||
CPUUsagePercent: 10,
|
||||
}).Error)
|
||||
require.NoError(t, db.DB(ctx).Create(&model.OpenFlareMetricSnapshot{
|
||||
NodeID: "node-a",
|
||||
CapturedAt: now.Add(-12 * time.Hour),
|
||||
CPUUsagePercent: 20,
|
||||
}).Error)
|
||||
|
||||
retentionDays := 7
|
||||
result, err := CleanupDatabaseObservability(ctx, DatabaseCleanupInput{
|
||||
Target: DatabaseCleanupTargetMetricSnapshots,
|
||||
RetentionDays: &retentionDays,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.False(t, result.DeleteAll)
|
||||
assert.Equal(t, int64(1), result.DeletedCount)
|
||||
|
||||
rows, err := model.ListOpenFlareMetricSnapshotsSince(ctx, "", time.Time{}, 0)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, rows, 1)
|
||||
assert.Equal(t, float64(20), rows[0].CPUUsagePercent)
|
||||
}
|
||||
|
||||
func TestCleanupDatabaseObservabilityDeletesAllRowsWhenRetentionMissing(t *testing.T) {
|
||||
ctx := setupDatabaseCleanupTestDB(t)
|
||||
now := time.Now().UTC()
|
||||
|
||||
require.NoError(t, db.DB(ctx).Create(&model.OpenFlareAccessLog{
|
||||
NodeID: "node-a",
|
||||
LoggedAt: now.Add(-3 * time.Hour),
|
||||
RemoteAddr: "203.0.113.1",
|
||||
Host: "example.com",
|
||||
Path: "/one",
|
||||
StatusCode: 200,
|
||||
}).Error)
|
||||
require.NoError(t, db.DB(ctx).Create(&model.OpenFlareAccessLog{
|
||||
NodeID: "node-a",
|
||||
LoggedAt: now.Add(-2 * time.Hour),
|
||||
RemoteAddr: "203.0.113.2",
|
||||
Host: "example.com",
|
||||
Path: "/two",
|
||||
StatusCode: 502,
|
||||
}).Error)
|
||||
|
||||
result, err := CleanupDatabaseObservability(ctx, DatabaseCleanupInput{
|
||||
Target: DatabaseCleanupTargetAccessLogs,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.DeleteAll)
|
||||
assert.Equal(t, int64(2), result.DeletedCount)
|
||||
|
||||
rows, err := model.ListOpenFlareAccessLogs(ctx, model.OpenFlareAccessLogQuery{Page: 0, PageSize: 10})
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, rows)
|
||||
}
|
||||
|
||||
func TestRunDatabaseAutoCleanupOnceDeletesAllObservabilityTargets(t *testing.T) {
|
||||
ctx := setupDatabaseCleanupTestDB(t)
|
||||
now := time.Now().UTC()
|
||||
|
||||
require.NoError(t, db.DB(ctx).Create(&model.OpenFlareAccessLog{
|
||||
NodeID: "node-a",
|
||||
LoggedAt: now.Add(-48 * time.Hour),
|
||||
RemoteAddr: "203.0.113.10",
|
||||
Host: "example.com",
|
||||
Path: "/access",
|
||||
StatusCode: 200,
|
||||
}).Error)
|
||||
require.NoError(t, db.DB(ctx).Create(&model.OpenFlareMetricSnapshot{
|
||||
NodeID: "node-a",
|
||||
CapturedAt: now.Add(-48 * time.Hour),
|
||||
CPUUsagePercent: 10,
|
||||
}).Error)
|
||||
require.NoError(t, db.DB(ctx).Create(&model.OpenFlareRequestReport{
|
||||
NodeID: "node-a",
|
||||
WindowStartedAt: now.Add(-49 * time.Hour),
|
||||
WindowEndedAt: now.Add(-48 * time.Hour),
|
||||
RequestCount: 15,
|
||||
}).Error)
|
||||
|
||||
previousEnabled := model.DatabaseAutoCleanupEnabled
|
||||
previousRetentionDays := model.DatabaseAutoCleanupRetentionDays
|
||||
model.DatabaseAutoCleanupEnabled = true
|
||||
model.DatabaseAutoCleanupRetentionDays = 1
|
||||
t.Cleanup(func() {
|
||||
model.DatabaseAutoCleanupEnabled = previousEnabled
|
||||
model.DatabaseAutoCleanupRetentionDays = previousRetentionDays
|
||||
})
|
||||
|
||||
summary, err := RunDatabaseAutoCleanupOnce(now)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, summary)
|
||||
require.Len(t, summary.Results, 3)
|
||||
|
||||
accessLogs, err := model.ListOpenFlareAccessLogs(ctx, model.OpenFlareAccessLogQuery{Page: 0, PageSize: 10})
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, accessLogs)
|
||||
|
||||
metricSnapshots, err := model.ListOpenFlareMetricSnapshotsSince(ctx, "", time.Time{}, 0)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, metricSnapshots)
|
||||
|
||||
requestReports, err := model.ListOpenFlareRequestReportsSince(ctx, "", time.Time{}, 0)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, requestReports)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package tasks is the single home for OpenFlare scheduled and background work
|
||||
// that runs inside the API process (goroutines plus robfig/cron), not the Asynq
|
||||
// worker or scheduler.
|
||||
//
|
||||
// Each job lives in its own file and registers via registerJob in init(). This
|
||||
// layout is intentional so jobs can migrate to a future task framework without
|
||||
// changing call sites: swap the registry implementation while keeping per-job files.
|
||||
//
|
||||
// Wire-up: bootstrap.RegisterOpenFlareBackgroundTasks imports this package so
|
||||
// init() registrations run; bootstrap.Init starts the cron scheduler when the
|
||||
// process serves the HTTP API.
|
||||
package tasks
|
||||
@@ -0,0 +1,97 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package tasks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
type cronJob struct {
|
||||
name string
|
||||
spec string
|
||||
run func(context.Context)
|
||||
}
|
||||
|
||||
var (
|
||||
registryMu sync.Mutex
|
||||
started bool
|
||||
cronJobs []cronJob
|
||||
cronRunner *cron.Cron
|
||||
jobCtx context.Context
|
||||
jobCancel context.CancelFunc
|
||||
)
|
||||
|
||||
func registerJob(name, cronSpec string, fn func(context.Context)) {
|
||||
registryMu.Lock()
|
||||
defer registryMu.Unlock()
|
||||
cronJobs = append(cronJobs, cronJob{name: name, spec: cronSpec, run: fn})
|
||||
}
|
||||
|
||||
// RegisterCronJob registers a cron job from another OpenFlare package (for example waf).
|
||||
// Prefer registerJob from init() inside this package when possible.
|
||||
func RegisterCronJob(name, cronSpec string, fn func(context.Context)) {
|
||||
registerJob(name, cronSpec, fn)
|
||||
}
|
||||
|
||||
// LogJobError records a failed OpenFlare cron job run.
|
||||
func LogJobError(ctx context.Context, name string, err error) {
|
||||
logger.ErrorF(ctx, "[OpenFlareTasks] %s failed: %v", name, err)
|
||||
}
|
||||
|
||||
// Start launches the cron scheduler in a background goroutine. Safe to call multiple times.
|
||||
func Start(ctx context.Context) {
|
||||
registryMu.Lock()
|
||||
defer registryMu.Unlock()
|
||||
if started {
|
||||
return
|
||||
}
|
||||
|
||||
jobCtx, jobCancel = context.WithCancel(context.Background())
|
||||
runner := cron.New()
|
||||
for _, job := range cronJobs {
|
||||
current := job
|
||||
if _, err := runner.AddFunc(current.spec, func() {
|
||||
current.run(jobCtx)
|
||||
}); err != nil {
|
||||
logger.ErrorF(ctx, "[OpenFlareTasks] register cron job %q failed: %v", current.name, err)
|
||||
continue
|
||||
}
|
||||
logger.InfoF(ctx, "[OpenFlareTasks] registered cron job %q (%s)", current.name, current.spec)
|
||||
}
|
||||
runner.Start()
|
||||
cronRunner = runner
|
||||
started = true
|
||||
}
|
||||
|
||||
// Stop shuts down the cron scheduler gracefully and cancels job contexts.
|
||||
func Stop() {
|
||||
registryMu.Lock()
|
||||
defer registryMu.Unlock()
|
||||
if !started || cronRunner == nil {
|
||||
return
|
||||
}
|
||||
|
||||
stopCtx := cronRunner.Stop()
|
||||
<-stopCtx.Done()
|
||||
if jobCancel != nil {
|
||||
jobCancel()
|
||||
}
|
||||
|
||||
cronRunner = nil
|
||||
started = false
|
||||
jobCtx = nil
|
||||
jobCancel = nil
|
||||
}
|
||||
|
||||
// ResetRegistryForTest clears scheduler state so unit tests can call Start again.
|
||||
func ResetRegistryForTest() {
|
||||
Stop()
|
||||
registryMu.Lock()
|
||||
defer registryMu.Unlock()
|
||||
cronJobs = nil
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package tasks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/tls"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerJob("ssl_renew", "0 0 * * *", func(ctx context.Context) {
|
||||
if err := runSSLRenewJob(ctx); err != nil {
|
||||
LogJobError(ctx, "ssl_renew", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func runSSLRenewJob(ctx context.Context) error {
|
||||
logger.InfoF(ctx, "[OpenFlareTasks] SSL renew job started")
|
||||
|
||||
certificates, err := model.ListTLSCertificates(ctx)
|
||||
if err != nil {
|
||||
logger.ErrorF(ctx, "[OpenFlareTasks] list certificates failed: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
due := tls.CertificatesDueForRenewal(certificates, now)
|
||||
if len(due) == 0 {
|
||||
logger.InfoF(ctx, "[OpenFlareTasks] SSL renew job completed: no certificates due")
|
||||
return nil
|
||||
}
|
||||
|
||||
var triggered int
|
||||
for _, cert := range due {
|
||||
logger.InfoF(ctx, "[OpenFlareTasks] renewing certificate id=%d domain=%s", cert.ID, cert.PrimaryDomain)
|
||||
if _, err := tls.RenewCertificate(ctx, cert.ID); err != nil {
|
||||
logger.ErrorF(ctx, "[OpenFlareTasks] renew certificate id=%d domain=%s failed: %v", cert.ID, cert.PrimaryDomain, err)
|
||||
continue
|
||||
}
|
||||
triggered++
|
||||
}
|
||||
|
||||
logger.InfoF(ctx, "[OpenFlareTasks] SSL renew job completed: triggered=%d eligible=%d", triggered, len(due))
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package tasks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/config"
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func setupSSLRenewTestDB(t *testing.T) func() {
|
||||
t.Helper()
|
||||
|
||||
sqliteDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
|
||||
DisableForeignKeyConstraintWhenMigrating: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, sqliteDB.AutoMigrate(&model.TLSCertificate{}))
|
||||
|
||||
db.SetDB(sqliteDB)
|
||||
oldSecret := config.Config.App.SessionSecret
|
||||
config.Config.App.SessionSecret = "test_session_secret_for_ssl_renew"
|
||||
return func() {
|
||||
db.SetDB(nil)
|
||||
config.Config.App.SessionSecret = oldSecret
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunSSLRenewJobTriggersDueCertificates(t *testing.T) {
|
||||
cleanup := setupSSLRenewTestDB(t)
|
||||
defer cleanup()
|
||||
ctx := context.Background()
|
||||
|
||||
now := time.Date(2026, 6, 18, 12, 0, 0, 0, time.UTC)
|
||||
due := &model.TLSCertificate{
|
||||
Name: "due-cert",
|
||||
Provider: "acme",
|
||||
AutoRenew: true,
|
||||
ApplyStatus: "ready",
|
||||
PrimaryDomain: "due.example.com",
|
||||
CertPEM: " ",
|
||||
KeyPEM: " ",
|
||||
NotAfter: now.Add(2 * 24 * time.Hour),
|
||||
}
|
||||
fresh := &model.TLSCertificate{
|
||||
Name: "fresh-cert",
|
||||
Provider: "acme",
|
||||
AutoRenew: true,
|
||||
ApplyStatus: "ready",
|
||||
PrimaryDomain: "fresh.example.com",
|
||||
CertPEM: " ",
|
||||
KeyPEM: " ",
|
||||
NotAfter: now.Add(30 * 24 * time.Hour),
|
||||
}
|
||||
require.NoError(t, model.CreateTLSCertificateRecord(ctx, due))
|
||||
require.NoError(t, model.CreateTLSCertificateRecord(ctx, fresh))
|
||||
|
||||
require.NoError(t, runSSLRenewJob(ctx))
|
||||
|
||||
renewed, err := model.GetTLSCertificateByID(ctx, due.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "applying", renewed.ApplyStatus)
|
||||
|
||||
unchanged, err := model.GetTLSCertificateByID(ctx, fresh.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "ready", unchanged.ApplyStatus)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package tasks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/uptimekuma"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
lastUptimeKumaSyncTime time.Time
|
||||
uptimeKumaSyncMutex sync.Mutex
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerJob("uptime_kuma_sync", "* * * * *", runUptimeKumaSyncJob)
|
||||
}
|
||||
|
||||
func runUptimeKumaSyncJob(ctx context.Context) {
|
||||
if !model.UptimeKumaEnabled {
|
||||
return
|
||||
}
|
||||
|
||||
interval := model.UptimeKumaSyncInterval
|
||||
if interval <= 0 {
|
||||
interval = 5
|
||||
}
|
||||
|
||||
if time.Since(lastUptimeKumaSyncTime) < time.Duration(interval)*time.Minute {
|
||||
return
|
||||
}
|
||||
|
||||
if !uptimeKumaSyncMutex.TryLock() {
|
||||
logger.WarnF(ctx, "[OpenFlareTasks] Uptime Kuma sync job is already running, skipping this scheduled run")
|
||||
return
|
||||
}
|
||||
defer uptimeKumaSyncMutex.Unlock()
|
||||
|
||||
logger.InfoF(ctx, "[OpenFlareTasks] Starting scheduled Uptime Kuma sync")
|
||||
if err := uptimekuma.SyncToUptimeKuma(ctx); err != nil {
|
||||
logger.ErrorF(ctx, "[OpenFlareTasks] Uptime Kuma sync failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
lastUptimeKumaSyncTime = time.Now()
|
||||
logger.InfoF(ctx, "[OpenFlareTasks] Uptime Kuma sync completed successfully")
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package tasks
|
||||
|
||||
import "context"
|
||||
|
||||
// RegisterWAFIPGroupSync registers the WAF IP group sync cron job without importing waf.
|
||||
func RegisterWAFIPGroupSync(syncFn func(context.Context) error) {
|
||||
RegisterCronJob("waf_ip_group_sync", "@every 5m", func(ctx context.Context) {
|
||||
if err := syncFn(ctx); err != nil {
|
||||
LogJobError(ctx, "waf_ip_group_sync", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user