refactor(backend): rename OpenFlare directory to lowercase openflare

This commit is contained in:
ryan
2026-08-30 17:43:23 +08:00
parent 06d5fedbfc
commit c93ff6674f
543 changed files with 819 additions and 819 deletions
@@ -0,0 +1,104 @@
// Copyright 2025 linux.do
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package migrate
import (
"context"
"database/sql"
"embed"
"io/fs"
"log"
"time"
"Wavelet/openflare/plugins/server/kernel/runtimeconfig"
"github.com/ClickHouse/clickhouse-go/v2"
"github.com/pressly/goose/v3"
)
const (
clickhouseMigrationDir = "clickhouse"
clickhouseGooseVersionTable = "goose_clickhouse_version"
clickhouseMaxExecTime = 60
clickhouseReadTimeoutFactor = 2
)
//go:embed clickhouse/*.sql
var clickhouseMigrationFS embed.FS
// UpClickHouse runs of_node_* goose migrations against ClickHouse when enabled.
func UpClickHouse() error {
if !runtimeconfig.ClickHouseEnabled() {
return nil
}
cfg := runtimeconfig.Get().ClickHouse
sqlDB := clickhouse.OpenDB(&clickhouse.Options{
Addr: cfg.Hosts,
Auth: clickhouse.Auth{
Database: cfg.Database,
Username: cfg.Username,
Password: cfg.Password,
},
Settings: clickhouse.Settings{
"max_execution_time": clickhouseMaxExecTime,
},
Compression: &clickhouse.Compression{
Method: clickhouse.CompressionLZ4,
},
DialTimeout: time.Duration(cfg.DialTimeout) * time.Second,
MaxOpenConns: cfg.MaxOpenConn,
MaxIdleConns: cfg.MaxIdleConn,
ConnMaxLifetime: time.Duration(cfg.ConnMaxLifetime) * time.Second,
ReadTimeout: time.Duration(cfg.DialTimeout*clickhouseReadTimeoutFactor) * time.Second,
BlockBufferSize: cfg.BlockBufferSize,
})
subFS, err := fs.Sub(clickhouseMigrationFS, clickhouseMigrationDir)
if err != nil {
closeClickHouseDB(sqlDB)
log.Printf("[ClickHouse] get sub fs failed: %v\n", err)
return err
}
provider, err := goose.NewProvider(
"clickhouse",
sqlDB,
subFS,
goose.WithTableName(clickhouseGooseVersionTable),
goose.WithDisableGlobalRegistry(true),
)
if err != nil {
closeClickHouseDB(sqlDB)
log.Printf("[ClickHouse] create goose provider failed: %v\n", err)
return err
}
if _, err := provider.GetDBVersion(context.Background()); err != nil {
closeClickHouseDB(sqlDB)
log.Printf("[ClickHouse] get goose version failed: %v\n", err)
return err
}
if _, err := provider.Up(context.Background()); err != nil {
closeClickHouseDB(sqlDB)
log.Printf("[ClickHouse] goose migrate failed: %v\n", err)
return err
}
if _, err := provider.GetDBVersion(context.Background()); err != nil {
closeClickHouseDB(sqlDB)
log.Printf("[ClickHouse] get migrated goose version failed: %v\n", err)
return err
}
closeClickHouseDB(sqlDB)
log.Println("[ClickHouse] goose migrate success")
return nil
}
func closeClickHouseDB(sqlDB *sql.DB) {
if err := sqlDB.Close(); err != nil {
log.Printf("[ClickHouse] close sql db failed: %v\n", err)
}
}
@@ -0,0 +1,20 @@
-- +goose Up
CREATE TABLE IF NOT EXISTS of_node_access_logs
(
id UInt64,
node_id String,
logged_at DateTime64(3, 'UTC'),
remote_addr String,
region String,
host String,
path String,
status_code Int32,
created_at DateTime64(3, 'UTC')
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(logged_at)
ORDER BY (node_id, logged_at, remote_addr, host, path, status_code)
SETTINGS index_granularity = 8192;
-- +goose Down
DROP TABLE IF EXISTS of_node_access_logs;
@@ -0,0 +1,92 @@
-- +goose Up
CREATE TABLE IF NOT EXISTS of_node_metric_snapshots
(
id UInt64,
node_id String,
captured_at DateTime64(3, 'UTC'),
cpu_usage_percent Float64,
memory_used_bytes Int64,
memory_total_bytes Int64,
storage_used_bytes Int64,
storage_total_bytes Int64,
disk_read_bytes Int64,
disk_write_bytes Int64,
network_rx_bytes Int64,
network_tx_bytes Int64,
created_at DateTime64(3, 'UTC')
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(captured_at)
ORDER BY (node_id, captured_at, id)
SETTINGS index_granularity = 8192;
CREATE TABLE IF NOT EXISTS of_node_request_reports
(
id UInt64,
node_id String,
window_started_at DateTime64(3, 'UTC'),
window_ended_at DateTime64(3, 'UTC'),
request_count Int64,
error_count Int64,
unique_visitor_count Int64,
status_codes_json String,
top_domains_json String,
source_countries_json String,
created_at DateTime64(3, 'UTC')
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(window_ended_at)
ORDER BY (node_id, window_ended_at, window_started_at, id)
SETTINGS index_granularity = 8192;
CREATE TABLE IF NOT EXISTS of_node_obs_openresty
(
id UInt64,
node_id String,
captured_at DateTime64(3, 'UTC'),
openresty_rx_bytes Int64,
openresty_tx_bytes Int64,
openresty_connections Int64,
created_at DateTime64(3, 'UTC')
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(captured_at)
ORDER BY (node_id, captured_at, id)
SETTINGS index_granularity = 8192;
CREATE TABLE IF NOT EXISTS of_node_obs_frps
(
id UInt64,
node_id String,
captured_at DateTime64(3, 'UTC'),
frps_connections Int32,
frps_proxy_count Int32,
frps_client_count Int32,
frps_proxies String,
created_at DateTime64(3, 'UTC')
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(captured_at)
ORDER BY (node_id, captured_at, id)
SETTINGS index_granularity = 8192;
CREATE TABLE IF NOT EXISTS of_node_obs_frpc
(
id UInt64,
node_id String,
captured_at DateTime64(3, 'UTC'),
tunnel_status String,
connected_relays_count Int32,
created_at DateTime64(3, 'UTC')
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(captured_at)
ORDER BY (node_id, captured_at, id)
SETTINGS index_granularity = 8192;
-- +goose Down
DROP TABLE IF EXISTS of_node_obs_frpc;
DROP TABLE IF EXISTS of_node_obs_frps;
DROP TABLE IF EXISTS of_node_obs_openresty;
DROP TABLE IF EXISTS of_node_request_reports;
DROP TABLE IF EXISTS of_node_metric_snapshots;
@@ -0,0 +1,17 @@
-- +goose Up
-- Add TTL policies to analytics tables so ClickHouse can expire rows automatically.
-- DateTime64 columns must be cast for TTL (ClickHouse requires DateTime/Date in TTL expr).
ALTER TABLE of_node_access_logs MODIFY TTL toDateTime(logged_at) + INTERVAL 90 DAY;
ALTER TABLE of_node_metric_snapshots MODIFY TTL toDateTime(captured_at) + INTERVAL 30 DAY;
ALTER TABLE of_node_request_reports MODIFY TTL toDateTime(window_ended_at) + INTERVAL 30 DAY;
ALTER TABLE of_node_obs_openresty MODIFY TTL toDateTime(captured_at) + INTERVAL 30 DAY;
ALTER TABLE of_node_obs_frps MODIFY TTL toDateTime(captured_at) + INTERVAL 30 DAY;
ALTER TABLE of_node_obs_frpc MODIFY TTL toDateTime(captured_at) + INTERVAL 30 DAY;
-- +goose Down
-- TTL changes cannot be safely reversed without recreating tables.
@@ -0,0 +1,28 @@
-- +goose Up
CREATE TABLE IF NOT EXISTS of_node_traffic_hourly
(
node_id String,
hour DateTime,
request_count UInt64,
error_count UInt64,
unique_visitor_count UInt64
)
ENGINE = SummingMergeTree()
PARTITION BY toYYYYMM(hour)
ORDER BY (node_id, hour);
CREATE MATERIALIZED VIEW IF NOT EXISTS of_node_traffic_hourly_mv
TO of_node_traffic_hourly
AS
SELECT
node_id,
toStartOfHour(window_ended_at) AS hour,
sum(request_count) AS request_count,
sum(error_count) AS error_count,
sum(unique_visitor_count) AS unique_visitor_count
FROM of_node_request_reports
GROUP BY node_id, hour;
-- +goose Down
DROP VIEW IF EXISTS of_node_traffic_hourly_mv;
DROP TABLE IF EXISTS of_node_traffic_hourly;
@@ -0,0 +1,80 @@
-- +goose Up
-- Hourly capacity rollups (avg CPU/memory + counter min/max for in-hour delta approximation).
-- Network/disk counters are cumulative; max-min within an hour approximates that hour's delta
-- (cross-hour continuity is intentionally approximate for dashboard trends).
CREATE TABLE IF NOT EXISTS of_node_metric_capacity_hourly
(
node_id String,
hour DateTime,
cpu_usage_sum SimpleAggregateFunction(sum, Float64),
cpu_usage_count SimpleAggregateFunction(sum, UInt64),
memory_usage_sum SimpleAggregateFunction(sum, Float64),
memory_usage_count SimpleAggregateFunction(sum, UInt64),
network_rx_min SimpleAggregateFunction(min, Int64),
network_rx_max SimpleAggregateFunction(max, Int64),
network_tx_min SimpleAggregateFunction(min, Int64),
network_tx_max SimpleAggregateFunction(max, Int64),
disk_read_min SimpleAggregateFunction(min, Int64),
disk_read_max SimpleAggregateFunction(max, Int64),
disk_write_min SimpleAggregateFunction(min, Int64),
disk_write_max SimpleAggregateFunction(max, Int64)
)
ENGINE = AggregatingMergeTree()
PARTITION BY toYYYYMM(hour)
ORDER BY (node_id, hour)
TTL hour + INTERVAL 30 DAY;
CREATE MATERIALIZED VIEW IF NOT EXISTS of_node_metric_capacity_hourly_mv
TO of_node_metric_capacity_hourly
AS
SELECT
node_id,
toStartOfHour(captured_at) AS hour,
sum(cpu_usage_percent) AS cpu_usage_sum,
toUInt64(count()) AS cpu_usage_count,
sum(if(memory_total_bytes > 0, (memory_used_bytes * 100.0) / memory_total_bytes, 0)) AS memory_usage_sum,
toUInt64(countIf(memory_total_bytes > 0)) AS memory_usage_count,
min(network_rx_bytes) AS network_rx_min,
max(network_rx_bytes) AS network_rx_max,
min(network_tx_bytes) AS network_tx_min,
max(network_tx_bytes) AS network_tx_max,
min(disk_read_bytes) AS disk_read_min,
max(disk_read_bytes) AS disk_read_max,
min(disk_write_bytes) AS disk_write_min,
max(disk_write_bytes) AS disk_write_max
FROM of_node_metric_snapshots
GROUP BY node_id, hour;
-- Hourly OpenResty counter rollups (min/max per node-hour for delta approximation).
CREATE TABLE IF NOT EXISTS of_node_openresty_hourly
(
node_id String,
hour DateTime,
openresty_rx_min SimpleAggregateFunction(min, Int64),
openresty_rx_max SimpleAggregateFunction(max, Int64),
openresty_tx_min SimpleAggregateFunction(min, Int64),
openresty_tx_max SimpleAggregateFunction(max, Int64)
)
ENGINE = AggregatingMergeTree()
PARTITION BY toYYYYMM(hour)
ORDER BY (node_id, hour)
TTL hour + INTERVAL 30 DAY;
CREATE MATERIALIZED VIEW IF NOT EXISTS of_node_openresty_hourly_mv
TO of_node_openresty_hourly
AS
SELECT
node_id,
toStartOfHour(captured_at) AS hour,
min(openresty_rx_bytes) AS openresty_rx_min,
max(openresty_rx_bytes) AS openresty_rx_max,
min(openresty_tx_bytes) AS openresty_tx_min,
max(openresty_tx_bytes) AS openresty_tx_max
FROM of_node_obs_openresty
GROUP BY node_id, hour;
-- +goose Down
DROP VIEW IF EXISTS of_node_openresty_hourly_mv;
DROP TABLE IF EXISTS of_node_openresty_hourly;
DROP VIEW IF EXISTS of_node_metric_capacity_hourly_mv;
DROP TABLE IF EXISTS of_node_metric_capacity_hourly;
@@ -0,0 +1,42 @@
-- +goose Up
-- Hourly traffic rollups: 30d TTL + UV aggregation semantics.
--
-- unique_visitor_count on of_node_request_reports is per short report window
-- (agent local distinct count for that window only). Summing those values in the
-- MV (and again via SummingMergeTree part merges) invents a "true UV" number that
-- double-counts visitors across windows. Prefer max() as a peak-window estimate;
-- still NOT distinct visitors across the hour — UI/API must not overclaim.
ALTER TABLE of_node_traffic_hourly
MODIFY TTL toDateTime(hour) + INTERVAL 30 DAY;
DROP VIEW IF EXISTS of_node_traffic_hourly_mv;
CREATE MATERIALIZED VIEW of_node_traffic_hourly_mv
TO of_node_traffic_hourly
AS
SELECT
node_id,
toStartOfHour(window_ended_at) AS hour,
sum(request_count) AS request_count,
sum(error_count) AS error_count,
-- Peak per-window UV estimate for the hour; not true cross-window distinct UV.
max(unique_visitor_count) AS unique_visitor_count
FROM of_node_request_reports
GROUP BY node_id, hour;
-- +goose Down
-- TTL reverse is not safe without table rewrite; restore prior MV definition only.
DROP VIEW IF EXISTS of_node_traffic_hourly_mv;
CREATE MATERIALIZED VIEW of_node_traffic_hourly_mv
TO of_node_traffic_hourly
AS
SELECT
node_id,
toStartOfHour(window_ended_at) AS hour,
sum(request_count) AS request_count,
sum(error_count) AS error_count,
sum(unique_visitor_count) AS unique_visitor_count
FROM of_node_request_reports
GROUP BY node_id, hour;
@@ -0,0 +1,66 @@
-- +goose Up
-- One-time historical backfill for hours not yet present in rollup tables.
-- MV only ingests rows after creation; without this, 24h charts rely on raw merge forever.
-- ANTI JOIN avoids double-counting hours already filled by the live MV.
INSERT INTO of_node_metric_capacity_hourly
SELECT
s.node_id,
toStartOfHour(s.captured_at) AS hour,
sum(s.cpu_usage_percent) AS cpu_usage_sum,
toUInt64(count()) AS cpu_usage_count,
sum(if(s.memory_total_bytes > 0, (s.memory_used_bytes * 100.0) / s.memory_total_bytes, 0)) AS memory_usage_sum,
toUInt64(countIf(s.memory_total_bytes > 0)) AS memory_usage_count,
min(s.network_rx_bytes) AS network_rx_min,
max(s.network_rx_bytes) AS network_rx_max,
min(s.network_tx_bytes) AS network_tx_min,
max(s.network_tx_bytes) AS network_tx_max,
min(s.disk_read_bytes) AS disk_read_min,
max(s.disk_read_bytes) AS disk_read_max,
min(s.disk_write_bytes) AS disk_write_min,
max(s.disk_write_bytes) AS disk_write_max
FROM of_node_metric_snapshots AS s
ANTI JOIN
(
SELECT
node_id,
hour
FROM of_node_metric_capacity_hourly
GROUP BY
node_id,
hour
) AS existing
ON s.node_id = existing.node_id AND toStartOfHour(s.captured_at) = existing.hour
WHERE s.captured_at >= now() - INTERVAL 30 DAY
GROUP BY
s.node_id,
hour;
INSERT INTO of_node_openresty_hourly
SELECT
s.node_id,
toStartOfHour(s.captured_at) AS hour,
min(s.openresty_rx_bytes) AS openresty_rx_min,
max(s.openresty_rx_bytes) AS openresty_rx_max,
min(s.openresty_tx_bytes) AS openresty_tx_min,
max(s.openresty_tx_bytes) AS openresty_tx_max
FROM of_node_obs_openresty AS s
ANTI JOIN
(
SELECT
node_id,
hour
FROM of_node_openresty_hourly
GROUP BY
node_id,
hour
) AS existing
ON s.node_id = existing.node_id AND toStartOfHour(s.captured_at) = existing.hour
WHERE s.captured_at >= now() - INTERVAL 30 DAY
GROUP BY
s.node_id,
hour;
-- +goose Down
-- Backfill is additive; down does not remove historical rollup rows (TTL still applies).
SELECT 1;
@@ -0,0 +1,5 @@
-- +goose Up
ALTER TABLE of_node_access_logs ADD COLUMN IF NOT EXISTS bytes_sent UInt64;
-- +goose Down
ALTER TABLE of_node_access_logs DROP COLUMN IF EXISTS bytes_sent;
@@ -0,0 +1,11 @@
-- +goose Up
-- L1 access log: request body/header length (接收数据) and optional request duration.
ALTER TABLE of_node_access_logs
ADD COLUMN IF NOT EXISTS request_length UInt64 DEFAULT 0;
ALTER TABLE of_node_access_logs
ADD COLUMN IF NOT EXISTS request_time_ms UInt32 DEFAULT 0;
-- +goose Down
ALTER TABLE of_node_access_logs DROP COLUMN IF EXISTS request_time_ms;
ALTER TABLE of_node_access_logs DROP COLUMN IF EXISTS request_length;
@@ -0,0 +1,149 @@
-- +goose Up
-- M5: L2 edge health table, L1 access-log hourly rollup, drop deprecated pre-aggregation tables.
CREATE TABLE IF NOT EXISTS of_node_edge_health
(
id UInt64,
node_id String,
captured_at DateTime64(3, 'UTC'),
status LowCardinality(String),
connections Int64,
created_at DateTime64(3, 'UTC')
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(captured_at)
ORDER BY (node_id, captured_at, id)
TTL toDateTime(captured_at) + INTERVAL 30 DAY
SETTINGS index_granularity = 8192;
-- Hourly business traffic from access logs (Server-side only; Agent never writes this).
-- SummingMergeTree merges request/error/bytes; UV is not stored (query raw for exact UV).
CREATE TABLE IF NOT EXISTS of_access_log_hourly
(
node_id String,
hour DateTime('UTC'),
host String,
request_count UInt64,
error_count UInt64,
bytes_sent UInt64,
request_length UInt64
)
ENGINE = SummingMergeTree()
PARTITION BY toYYYYMM(hour)
ORDER BY (node_id, hour, host)
TTL hour + INTERVAL 90 DAY
SETTINGS index_granularity = 8192;
CREATE MATERIALIZED VIEW IF NOT EXISTS of_access_log_hourly_mv
TO of_access_log_hourly
AS
SELECT
node_id,
toStartOfHour(logged_at) AS hour,
host,
toUInt64(count()) AS request_count,
toUInt64(countIf(status_code >= 500)) AS error_count,
sum(bytes_sent) AS bytes_sent,
sum(request_length) AS request_length
FROM of_node_access_logs
GROUP BY node_id, hour, host;
-- Deprecated pre-aggregation paths (business traffic is access logs; OR throughput is not authoritative).
DROP VIEW IF EXISTS of_node_traffic_hourly_mv;
DROP TABLE IF EXISTS of_node_traffic_hourly;
DROP VIEW IF EXISTS of_node_openresty_hourly_mv;
DROP TABLE IF EXISTS of_node_openresty_hourly;
DROP TABLE IF EXISTS of_node_request_reports;
DROP TABLE IF EXISTS of_node_obs_openresty;
-- +goose Down
CREATE TABLE IF NOT EXISTS of_node_obs_openresty
(
id UInt64,
node_id String,
captured_at DateTime64(3, 'UTC'),
openresty_rx_bytes Int64,
openresty_tx_bytes Int64,
openresty_connections Int64,
created_at DateTime64(3, 'UTC')
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(captured_at)
ORDER BY (node_id, captured_at, id)
TTL toDateTime(captured_at) + INTERVAL 30 DAY
SETTINGS index_granularity = 8192;
CREATE TABLE IF NOT EXISTS of_node_request_reports
(
id UInt64,
node_id String,
window_started_at DateTime64(3, 'UTC'),
window_ended_at DateTime64(3, 'UTC'),
request_count Int64,
error_count Int64,
unique_visitor_count Int64,
status_codes_json String,
top_domains_json String,
source_countries_json String,
created_at DateTime64(3, 'UTC')
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(window_ended_at)
ORDER BY (node_id, window_ended_at, window_started_at, id)
TTL toDateTime(window_ended_at) + INTERVAL 30 DAY
SETTINGS index_granularity = 8192;
CREATE TABLE IF NOT EXISTS of_node_traffic_hourly
(
node_id String,
hour DateTime,
request_count UInt64,
error_count UInt64,
unique_visitor_count UInt64
)
ENGINE = SummingMergeTree()
PARTITION BY toYYYYMM(hour)
ORDER BY (node_id, hour);
CREATE MATERIALIZED VIEW IF NOT EXISTS of_node_traffic_hourly_mv
TO of_node_traffic_hourly
AS
SELECT
node_id,
toStartOfHour(window_ended_at) AS hour,
sum(request_count) AS request_count,
sum(error_count) AS error_count,
max(unique_visitor_count) AS unique_visitor_count
FROM of_node_request_reports
GROUP BY node_id, hour;
CREATE TABLE IF NOT EXISTS of_node_openresty_hourly
(
node_id String,
hour DateTime,
openresty_rx_min SimpleAggregateFunction(min, Int64),
openresty_rx_max SimpleAggregateFunction(max, Int64),
openresty_tx_min SimpleAggregateFunction(min, Int64),
openresty_tx_max SimpleAggregateFunction(max, Int64)
)
ENGINE = AggregatingMergeTree()
PARTITION BY toYYYYMM(hour)
ORDER BY (node_id, hour)
TTL hour + INTERVAL 30 DAY;
CREATE MATERIALIZED VIEW IF NOT EXISTS of_node_openresty_hourly_mv
TO of_node_openresty_hourly
AS
SELECT
node_id,
toStartOfHour(captured_at) AS hour,
min(openresty_rx_bytes) AS openresty_rx_min,
max(openresty_rx_bytes) AS openresty_rx_max,
min(openresty_tx_bytes) AS openresty_tx_min,
max(openresty_tx_bytes) AS openresty_tx_max
FROM of_node_obs_openresty
GROUP BY node_id, hour;
DROP VIEW IF EXISTS of_access_log_hourly_mv;
DROP TABLE IF EXISTS of_access_log_hourly;
DROP TABLE IF EXISTS of_node_edge_health;
@@ -0,0 +1,40 @@
-- +goose Up
-- One-time historical backfill for of_access_log_hourly.
-- MV only ingests rows after creation; without this, 24h charts fall back to raw access logs.
-- ANTI JOIN avoids double-counting (node_id, hour, host) already filled by the live MV.
-- UV is intentionally NOT stored in of_access_log_hourly (SummingMergeTree counts only).
INSERT INTO of_access_log_hourly
SELECT
s.node_id,
toStartOfHour(s.logged_at) AS hour,
s.host,
toUInt64(count()) AS request_count,
toUInt64(countIf(s.status_code >= 500)) AS error_count,
sum(s.bytes_sent) AS bytes_sent,
sum(s.request_length) AS request_length
FROM of_node_access_logs AS s
ANTI JOIN
(
SELECT
node_id,
hour,
host
FROM of_access_log_hourly
GROUP BY
node_id,
hour,
host
) AS existing
ON s.node_id = existing.node_id
AND toStartOfHour(s.logged_at) = existing.hour
AND s.host = existing.host
WHERE s.logged_at >= now() - INTERVAL 90 DAY
GROUP BY
s.node_id,
hour,
s.host;
-- +goose Down
-- Backfill is additive; down does not remove historical rollup rows (TTL still applies).
SELECT 1;
@@ -0,0 +1,6 @@
-- +goose Up
ALTER TABLE of_node_access_logs
ADD COLUMN IF NOT EXISTS user_agent String DEFAULT '';
-- +goose Down
ALTER TABLE of_node_access_logs DROP COLUMN IF EXISTS user_agent;
@@ -0,0 +1,6 @@
-- +goose Up
ALTER TABLE of_node_access_logs
ADD COLUMN IF NOT EXISTS cache_status String DEFAULT '';
-- +goose Down
ALTER TABLE of_node_access_logs DROP COLUMN IF EXISTS cache_status;
@@ -0,0 +1,52 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package migrate
import (
"strings"
"testing"
"Wavelet/openflare/plugins/server/kernel/runtimeconfig"
"github.com/pressly/goose/v3"
)
func TestClickHouseMigrationFilesEmbedded(t *testing.T) {
entries, err := clickhouseMigrationFS.ReadDir(clickhouseMigrationDir)
if err != nil {
t.Fatalf("ReadDir(%q) error = %v", clickhouseMigrationDir, err)
}
if len(entries) == 0 {
t.Fatal("expected embedded ClickHouse migrations, got none")
}
foundNodeLogs := false
for _, entry := range entries {
if entry.IsDir() {
continue
}
if strings.Contains(entry.Name(), "user_access") {
t.Errorf("user access log migration %s must not be embedded", entry.Name())
}
if entry.Name() == "202606200001_create_node_access_logs.sql" {
foundNodeLogs = true
}
}
if !foundNodeLogs {
t.Fatal("expected 202606200001_create_node_access_logs.sql in embedded migrations")
}
}
func TestClickHouseGooseDialect(t *testing.T) {
if err := goose.SetDialect("clickhouse"); err != nil {
t.Fatalf("SetDialect(clickhouse) error = %v", err)
}
}
func TestUpSkipsWhenDisabled(t *testing.T) {
t.Cleanup(runtimeconfig.Override(runtimeconfig.DatabaseEnabled(), false))
if err := UpClickHouse(); err != nil {
t.Fatalf("UpClickHouse() error = %v, want nil when ClickHouse disabled", err)
}
}
@@ -0,0 +1,579 @@
-- +goose Up
CREATE TABLE IF NOT EXISTS of_acme_accounts (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL DEFAULT '',
url VARCHAR(255) NOT NULL DEFAULT '',
private_key TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS of_apply_logs (
id BIGSERIAL PRIMARY KEY,
node_id VARCHAR(64) NOT NULL,
version VARCHAR(32) NOT NULL,
result VARCHAR(32) NOT NULL,
message TEXT,
checksum VARCHAR(64) NOT NULL DEFAULT '',
main_config_checksum VARCHAR(64) NOT NULL DEFAULT '',
route_config_checksum VARCHAR(64) NOT NULL DEFAULT '',
support_file_count INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_of_apply_logs_created_at ON of_apply_logs(created_at);
CREATE INDEX IF NOT EXISTS idx_of_apply_logs_node_id ON of_apply_logs(node_id);
CREATE TABLE IF NOT EXISTS of_cf_connections (
id BIGSERIAL PRIMARY KEY,
source VARCHAR(32) NOT NULL DEFAULT '',
dns_account_id BIGINT,
"authorization" TEXT NOT NULL DEFAULT '',
status VARCHAR(16) NOT NULL DEFAULT '',
verified_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_of_cf_connections_dns_account_id ON of_cf_connections (dns_account_id);
CREATE TABLE IF NOT EXISTS of_cf_pointing_groups (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(128) NOT NULL,
primary_node_id BIGINT NOT NULL,
backup_node_id BIGINT,
active_node_id BIGINT NOT NULL,
default_proxied BOOLEAN NOT NULL DEFAULT FALSE,
enabled BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_of_cf_pointing_groups_active_node_id ON of_cf_pointing_groups (active_node_id);
CREATE INDEX IF NOT EXISTS idx_of_cf_pointing_groups_backup_node_id ON of_cf_pointing_groups (backup_node_id);
CREATE INDEX IF NOT EXISTS idx_of_cf_pointing_groups_primary_node_id ON of_cf_pointing_groups (primary_node_id);
CREATE TABLE IF NOT EXISTS of_cf_pointing_members (
id BIGSERIAL PRIMARY KEY,
group_id BIGINT NOT NULL,
zone_domain_id BIGINT NOT NULL,
proxied BOOLEAN NOT NULL DEFAULT FALSE,
cf_zone_id VARCHAR(64) NOT NULL DEFAULT '',
cf_record_id VARCHAR(64) NOT NULL DEFAULT '',
desired_ip VARCHAR(64) NOT NULL DEFAULT '',
sync_status VARCHAR(16) NOT NULL DEFAULT 'pending',
last_error TEXT NOT NULL DEFAULT '',
synced_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_of_cf_pointing_members_group_id ON of_cf_pointing_members (group_id);
CREATE UNIQUE INDEX IF NOT EXISTS idx_of_cf_pointing_members_zone_domain_id ON of_cf_pointing_members (zone_domain_id);
CREATE TABLE IF NOT EXISTS of_config_versions (
version VARCHAR(32) PRIMARY KEY,
snapshot_json TEXT NOT NULL,
main_config TEXT NOT NULL DEFAULT '',
rendered_config TEXT NOT NULL,
support_files_json TEXT NOT NULL DEFAULT '[]',
checksum VARCHAR(64) NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT FALSE,
created_by VARCHAR(64) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_of_config_versions_is_active ON of_config_versions (is_active);
CREATE TABLE IF NOT EXISTS of_dns_accounts (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
type VARCHAR(64) NOT NULL,
"authorization" TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS of_node_access_logs (
id BIGINT NOT NULL,
node_id VARCHAR(64) NOT NULL DEFAULT '',
logged_at TIMESTAMPTZ NOT NULL,
remote_addr VARCHAR(128) NOT NULL DEFAULT '',
region VARCHAR(128) NOT NULL DEFAULT '',
host VARCHAR(255) NOT NULL DEFAULT '',
path VARCHAR(2048) NOT NULL DEFAULT '',
user_agent TEXT NOT NULL DEFAULT '',
cache_status VARCHAR(64) NOT NULL DEFAULT '',
status_code INTEGER NOT NULL DEFAULT 0,
bytes_sent BIGINT NOT NULL DEFAULT 0,
request_length BIGINT NOT NULL DEFAULT 0,
request_time_ms INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id, logged_at)
) PARTITION BY RANGE (logged_at);
CREATE INDEX IF NOT EXISTS idx_of_node_access_logs_host ON of_node_access_logs (host, logged_at DESC);
CREATE INDEX IF NOT EXISTS idx_of_node_access_logs_host_lower ON of_node_access_logs (lower(trim(host)));
CREATE INDEX IF NOT EXISTS idx_of_node_access_logs_logged_at ON of_node_access_logs (logged_at DESC, id DESC);
CREATE INDEX IF NOT EXISTS idx_of_node_access_logs_node_id ON of_node_access_logs (node_id, logged_at DESC);
CREATE INDEX IF NOT EXISTS idx_of_node_access_logs_remote_addr ON of_node_access_logs (remote_addr, logged_at DESC);
CREATE INDEX IF NOT EXISTS idx_of_node_access_logs_status_code ON of_node_access_logs (status_code, logged_at DESC);
-- +goose StatementBegin
DO $$
DECLARE
d date;
BEGIN
FOR d IN SELECT generate_series(date_trunc('month', now())::date, (date_trunc('month', now()) + interval '2 months')::date, interval '1 month')::date
LOOP
EXECUTE format('CREATE TABLE IF NOT EXISTS of_node_access_logs_%s PARTITION OF of_node_access_logs FOR VALUES FROM (%L) TO (%L)',
to_char(d, 'YYYYMM'), d, d + interval '1 month');
END LOOP;
END $$;
-- +goose StatementEnd
CREATE TABLE IF NOT EXISTS of_node_edge_health (
id BIGINT NOT NULL PRIMARY KEY,
node_id VARCHAR(64) NOT NULL DEFAULT '',
captured_at TIMESTAMPTZ NOT NULL,
status VARCHAR(64) NOT NULL DEFAULT '',
connections BIGINT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_of_node_edge_health_node ON of_node_edge_health (node_id, captured_at DESC);
CREATE TABLE IF NOT EXISTS of_node_health_events (
id BIGSERIAL PRIMARY KEY,
node_id VARCHAR(64) NOT NULL,
event_type VARCHAR(64) NOT NULL,
severity VARCHAR(16) NOT NULL,
status VARCHAR(16) NOT NULL,
message TEXT,
first_triggered_at TIMESTAMPTZ NOT NULL,
last_triggered_at TIMESTAMPTZ NOT NULL,
reported_at TIMESTAMPTZ NOT NULL,
resolved_at TIMESTAMPTZ,
metadata_json TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_of_node_health_events_event_type ON of_node_health_events (event_type);
CREATE INDEX IF NOT EXISTS idx_of_node_health_events_first_triggered_at ON of_node_health_events (first_triggered_at);
CREATE INDEX IF NOT EXISTS idx_of_node_health_events_last_triggered_at ON of_node_health_events (last_triggered_at);
CREATE INDEX IF NOT EXISTS idx_of_node_health_events_node_id ON of_node_health_events (node_id);
CREATE INDEX IF NOT EXISTS idx_of_node_health_events_reported_at ON of_node_health_events (reported_at);
CREATE INDEX IF NOT EXISTS idx_of_node_health_events_resolved_at ON of_node_health_events (resolved_at);
CREATE INDEX IF NOT EXISTS idx_of_node_health_events_status ON of_node_health_events (status);
CREATE TABLE IF NOT EXISTS of_node_metric_snapshots (
id BIGINT NOT NULL PRIMARY KEY,
node_id VARCHAR(64) NOT NULL DEFAULT '',
captured_at TIMESTAMPTZ NOT NULL,
cpu_usage_percent DOUBLE PRECISION NOT NULL DEFAULT 0,
memory_used_bytes BIGINT NOT NULL DEFAULT 0,
memory_total_bytes BIGINT NOT NULL DEFAULT 0,
storage_used_bytes BIGINT NOT NULL DEFAULT 0,
storage_total_bytes BIGINT NOT NULL DEFAULT 0,
disk_read_bytes BIGINT NOT NULL DEFAULT 0,
disk_write_bytes BIGINT NOT NULL DEFAULT 0,
network_rx_bytes BIGINT NOT NULL DEFAULT 0,
network_tx_bytes BIGINT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_of_node_metric_snapshots_node ON of_node_metric_snapshots (node_id, captured_at DESC);
CREATE TABLE IF NOT EXISTS of_node_obs_frpc (
id BIGINT NOT NULL PRIMARY KEY,
node_id VARCHAR(64) NOT NULL DEFAULT '',
captured_at TIMESTAMPTZ NOT NULL,
tunnel_status VARCHAR(16) NOT NULL DEFAULT '',
connected_relays_count INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_of_node_obs_frpc_node ON of_node_obs_frpc (node_id, captured_at DESC);
CREATE TABLE IF NOT EXISTS of_node_obs_frps (
id BIGINT NOT NULL PRIMARY KEY,
node_id VARCHAR(64) NOT NULL DEFAULT '',
captured_at TIMESTAMPTZ NOT NULL,
frps_connections INTEGER NOT NULL DEFAULT 0,
frps_proxy_count INTEGER NOT NULL DEFAULT 0,
frps_client_count INTEGER NOT NULL DEFAULT 0,
frps_proxies TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_of_node_obs_frps_node ON of_node_obs_frps (node_id, captured_at DESC);
CREATE TABLE IF NOT EXISTS of_node_system_profiles (
id BIGSERIAL PRIMARY KEY,
node_id VARCHAR(64) NOT NULL,
hostname VARCHAR(255) NOT NULL DEFAULT '',
os_name VARCHAR(128) NOT NULL DEFAULT '',
os_version VARCHAR(128) NOT NULL DEFAULT '',
kernel_version VARCHAR(128) NOT NULL DEFAULT '',
architecture VARCHAR(64) NOT NULL DEFAULT '',
cpu_model VARCHAR(255) NOT NULL DEFAULT '',
cpu_cores INTEGER NOT NULL DEFAULT 0,
total_memory_bytes BIGINT NOT NULL DEFAULT 0,
total_disk_bytes BIGINT NOT NULL DEFAULT 0,
uptime_seconds BIGINT NOT NULL DEFAULT 0,
reported_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_of_node_system_profiles_node_id ON of_node_system_profiles (node_id);
CREATE INDEX IF NOT EXISTS idx_of_node_system_profiles_reported_at ON of_node_system_profiles (reported_at);
CREATE TABLE IF NOT EXISTS of_nodes (
id BIGSERIAL PRIMARY KEY,
node_id VARCHAR(64) NOT NULL,
name VARCHAR(128) NOT NULL,
ip VARCHAR(64) NOT NULL DEFAULT '',
ip_manual_override BOOLEAN NOT NULL DEFAULT FALSE,
geo_name VARCHAR(128) NOT NULL DEFAULT '',
geo_latitude DOUBLE PRECISION,
geo_longitude DOUBLE PRECISION,
geo_manual_override BOOLEAN NOT NULL DEFAULT FALSE,
access_token VARCHAR(128) NOT NULL DEFAULT '',
auto_update_enabled BOOLEAN NOT NULL DEFAULT FALSE,
update_requested BOOLEAN NOT NULL DEFAULT FALSE,
update_channel VARCHAR(16) NOT NULL DEFAULT 'stable',
update_tag VARCHAR(64) NOT NULL DEFAULT '',
restart_openresty_requested BOOLEAN NOT NULL DEFAULT FALSE,
version VARCHAR(64) NOT NULL DEFAULT '',
ext_version VARCHAR(64) NOT NULL DEFAULT '',
openresty_status VARCHAR(16) NOT NULL DEFAULT 'unknown',
openresty_message TEXT,
status VARCHAR(16) NOT NULL DEFAULT 'offline',
current_version VARCHAR(32) NOT NULL DEFAULT '',
last_seen_at TIMESTAMPTZ,
last_error TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
node_type VARCHAR(32) NOT NULL DEFAULT 'edge_node',
relay_bind_port INTEGER NOT NULL DEFAULT 0,
relay_vhost_http_port INTEGER NOT NULL DEFAULT 0,
relay_auth_token VARCHAR(128) NOT NULL DEFAULT '',
relay_agent_access_addr VARCHAR(255) NOT NULL DEFAULT '',
relay_client_access_addr VARCHAR(255) NOT NULL DEFAULT '',
relay_client_proxy_url VARCHAR(512) NOT NULL DEFAULT '',
capabilities_json TEXT NOT NULL DEFAULT '[]',
relay_status VARCHAR(16) NOT NULL DEFAULT 'unknown',
relay_web_server_enabled BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE INDEX IF NOT EXISTS idx_of_nodes_access_token ON of_nodes (access_token);
CREATE UNIQUE INDEX IF NOT EXISTS idx_of_nodes_node_id ON of_nodes (node_id);
CREATE TABLE IF NOT EXISTS of_origins (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
address VARCHAR(255) NOT NULL,
remark VARCHAR(255) NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_of_origins_address ON of_origins (address);
CREATE TABLE IF NOT EXISTS of_pages_deployment_files (
id BIGSERIAL PRIMARY KEY,
deployment_id BIGINT NOT NULL,
path VARCHAR(2048) NOT NULL,
size BIGINT NOT NULL DEFAULT 0,
checksum VARCHAR(64) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_of_pages_deployment_files_deployment_id ON of_pages_deployment_files (deployment_id);
CREATE TABLE IF NOT EXISTS of_pages_deployments (
id BIGSERIAL PRIMARY KEY,
project_id BIGINT NOT NULL,
deployment_number INTEGER NOT NULL,
checksum VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL DEFAULT 'uploaded',
artifact_path VARCHAR(2048) NOT NULL DEFAULT '',
file_count INTEGER NOT NULL DEFAULT 0,
total_size BIGINT NOT NULL DEFAULT 0,
created_by VARCHAR(64) NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
activated_at TIMESTAMPTZ,
upload_id BIGINT NOT NULL DEFAULT 0,
source_type VARCHAR(32) NOT NULL DEFAULT '',
source_identity CHAR(64),
source_revision CHAR(64),
source_label VARCHAR(255) NOT NULL DEFAULT '',
source_meta TEXT NOT NULL DEFAULT '',
trigger_type VARCHAR(32) NOT NULL DEFAULT ''
);
CREATE INDEX IF NOT EXISTS idx_of_pages_deployments_checksum ON of_pages_deployments (checksum);
CREATE INDEX IF NOT EXISTS idx_of_pages_deployments_project_id ON of_pages_deployments (project_id);
CREATE UNIQUE INDEX IF NOT EXISTS idx_of_pages_deployments_project_number
ON of_pages_deployments (project_id, deployment_number);
CREATE UNIQUE INDEX IF NOT EXISTS idx_of_pages_deployments_source_revision
ON of_pages_deployments (project_id, source_identity, source_revision)
WHERE source_identity IS NOT NULL AND source_revision IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_of_pages_deployments_status ON of_pages_deployments (status);
CREATE INDEX IF NOT EXISTS idx_of_pages_deployments_upload_id ON of_pages_deployments (upload_id);
CREATE TABLE IF NOT EXISTS of_pages_project_source_runtime (
source_id BIGINT PRIMARY KEY,
etag VARCHAR(512) NOT NULL DEFAULT '',
last_seen_revision CHAR(64) NOT NULL DEFAULT '',
last_seen_detail TEXT NOT NULL DEFAULT '',
last_applied_revision CHAR(64) NOT NULL DEFAULT '',
last_applied_detail TEXT NOT NULL DEFAULT '',
sync_status VARCHAR(32) NOT NULL DEFAULT '',
last_error TEXT NOT NULL DEFAULT '',
last_checked_at TIMESTAMPTZ,
last_synced_at TIMESTAMPTZ,
next_check_at TIMESTAMPTZ,
lease_expires_at TIMESTAMPTZ,
lease_token VARCHAR(64) NOT NULL DEFAULT '',
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_of_pages_project_source_runtime_next_check_at
ON of_pages_project_source_runtime (next_check_at);
CREATE TABLE IF NOT EXISTS of_pages_project_sources (
id BIGSERIAL PRIMARY KEY,
project_id BIGINT NOT NULL,
source_type VARCHAR(32) NOT NULL DEFAULT '',
remote_url TEXT NOT NULL DEFAULT '',
allow_insecure BOOLEAN NOT NULL DEFAULT FALSE,
github_repository VARCHAR(255) NOT NULL DEFAULT '',
release_selector VARCHAR(16) NOT NULL DEFAULT '',
release_tag VARCHAR(255) NOT NULL DEFAULT '',
asset_name VARCHAR(255) NOT NULL DEFAULT '',
auto_update_enabled BOOLEAN NOT NULL DEFAULT FALSE,
check_interval_minutes INTEGER NOT NULL DEFAULT 0,
config_version INTEGER NOT NULL DEFAULT 0,
source_identity CHAR(64) NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_of_pages_project_sources_project_id
ON of_pages_project_sources (project_id);
CREATE TABLE IF NOT EXISTS of_pages_projects (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
slug VARCHAR(128) NOT NULL,
description TEXT NOT NULL DEFAULT '',
enabled BOOLEAN NOT NULL DEFAULT TRUE,
spa_fallback_enabled BOOLEAN NOT NULL DEFAULT FALSE,
spa_fallback_path VARCHAR(512) NOT NULL DEFAULT '/index.html',
api_proxy_enabled BOOLEAN NOT NULL DEFAULT FALSE,
api_proxy_path VARCHAR(255) NOT NULL DEFAULT '',
api_proxy_pass VARCHAR(2048) NOT NULL DEFAULT '',
api_proxy_rewrite VARCHAR(255) NOT NULL DEFAULT '',
active_deployment_id BIGINT,
root_dir VARCHAR(512) NOT NULL DEFAULT '',
entry_file VARCHAR(512) NOT NULL DEFAULT 'index.html',
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
content_config_version INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_of_pages_projects_active_deployment_id ON of_pages_projects (active_deployment_id);
CREATE UNIQUE INDEX IF NOT EXISTS idx_of_pages_projects_slug ON of_pages_projects (slug);
CREATE TABLE IF NOT EXISTS of_proxy_routes (
id BIGSERIAL PRIMARY KEY,
site_name VARCHAR(255) NOT NULL DEFAULT '',
origin_id BIGINT,
origin_url VARCHAR(2048) NOT NULL,
origin_host VARCHAR(255) NOT NULL DEFAULT '',
upstreams TEXT NOT NULL DEFAULT '[]',
enabled BOOLEAN NOT NULL DEFAULT TRUE,
enable_https BOOLEAN NOT NULL DEFAULT FALSE,
redirect_http BOOLEAN NOT NULL DEFAULT FALSE,
limit_conn_per_server INTEGER NOT NULL DEFAULT 0,
limit_conn_per_ip INTEGER NOT NULL DEFAULT 0,
limit_rate VARCHAR(32) NOT NULL DEFAULT '',
cache_enabled BOOLEAN NOT NULL DEFAULT FALSE,
cache_policy VARCHAR(32) NOT NULL DEFAULT '',
cache_rules TEXT NOT NULL DEFAULT '[]',
custom_headers TEXT NOT NULL DEFAULT '[]',
basic_auth_enabled BOOLEAN NOT NULL DEFAULT FALSE,
basic_auth_username VARCHAR(255) NOT NULL DEFAULT '',
basic_auth_password VARCHAR(255) NOT NULL DEFAULT '',
upstream_type VARCHAR(32) NOT NULL DEFAULT 'direct',
tunnel_node_id BIGINT,
tunnel_target_addr VARCHAR(512) NOT NULL DEFAULT '',
tunnel_target_protocol VARCHAR(16) NOT NULL DEFAULT '',
pages_project_id BIGINT,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
limit_req_per_ip VARCHAR(32) NOT NULL DEFAULT ''
);
CREATE INDEX IF NOT EXISTS idx_of_proxy_routes_origin_id ON of_proxy_routes (origin_id);
CREATE INDEX IF NOT EXISTS idx_of_proxy_routes_pages_project_id ON of_proxy_routes (pages_project_id);
CREATE UNIQUE INDEX IF NOT EXISTS idx_of_proxy_routes_site_name ON of_proxy_routes (site_name);
CREATE INDEX IF NOT EXISTS idx_of_proxy_routes_tunnel_node_id ON of_proxy_routes (tunnel_node_id);
CREATE TABLE IF NOT EXISTS of_tls_certificates (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
cert_pem TEXT NOT NULL,
key_pem TEXT NOT NULL,
not_before TIMESTAMPTZ,
not_after TIMESTAMPTZ,
remark VARCHAR(255) NOT NULL DEFAULT '',
provider VARCHAR(64) NOT NULL DEFAULT 'upload',
acme_account_id BIGINT NOT NULL DEFAULT 0,
dns_account_id BIGINT NOT NULL DEFAULT 0,
key_algorithm VARCHAR(32) NOT NULL DEFAULT '',
auto_renew BOOLEAN NOT NULL DEFAULT FALSE,
primary_domain VARCHAR(255) NOT NULL DEFAULT '',
other_domains TEXT NOT NULL DEFAULT '',
disable_cname BOOLEAN NOT NULL DEFAULT FALSE,
skip_dns BOOLEAN NOT NULL DEFAULT FALSE,
dns1 VARCHAR(128) NOT NULL DEFAULT '',
dns2 VARCHAR(128) NOT NULL DEFAULT '',
apply_status VARCHAR(64) NOT NULL DEFAULT 'ready',
apply_message TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_of_tls_certificates_name ON of_tls_certificates (name);
CREATE TABLE IF NOT EXISTS of_waf_ip_groups (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
type VARCHAR(32) NOT NULL,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
ip_list TEXT NOT NULL DEFAULT '[]',
auto_config TEXT NOT NULL DEFAULT '{}',
ext_ips TEXT NOT NULL DEFAULT '[]',
subscription_url VARCHAR(2048) NOT NULL DEFAULT '',
subscription_format VARCHAR(32) NOT NULL DEFAULT 'text',
subscription_mapping_rule VARCHAR(255) NOT NULL DEFAULT '',
sync_interval_minutes INTEGER NOT NULL DEFAULT 1440,
last_synced_at TIMESTAMPTZ,
next_sync_at TIMESTAMPTZ,
last_sync_status VARCHAR(32) NOT NULL DEFAULT '',
last_sync_message TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_of_waf_ip_groups_next_sync_at ON of_waf_ip_groups (next_sync_at);
CREATE INDEX IF NOT EXISTS idx_of_waf_ip_groups_type ON of_waf_ip_groups (type);
CREATE TABLE IF NOT EXISTS of_waf_rule_group_bindings (
id BIGSERIAL PRIMARY KEY,
rule_group_id BIGINT NOT NULL,
proxy_route_id BIGINT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
sequence INTEGER NOT NULL DEFAULT 0
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_of_waf_group_route ON of_waf_rule_group_bindings (rule_group_id, proxy_route_id);
CREATE INDEX IF NOT EXISTS idx_of_waf_rule_group_bindings_proxy_route_id ON of_waf_rule_group_bindings (proxy_route_id);
CREATE TABLE IF NOT EXISTS of_waf_rule_groups (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
is_global BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
graph TEXT NOT NULL DEFAULT '',
revision BIGINT NOT NULL DEFAULT 1
);
CREATE INDEX IF NOT EXISTS idx_of_waf_rule_groups_is_global ON of_waf_rule_groups (is_global);
CREATE TABLE IF NOT EXISTS of_zone_domains (
id BIGSERIAL PRIMARY KEY,
zone_id BIGINT NOT NULL,
proxy_route_id BIGINT,
domain VARCHAR(255) NOT NULL,
cert_id BIGINT,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_of_zone_domains_cert_id ON of_zone_domains (cert_id);
CREATE UNIQUE INDEX IF NOT EXISTS idx_of_zone_domains_domain ON of_zone_domains (domain);
CREATE INDEX IF NOT EXISTS idx_of_zone_domains_proxy_route_id ON of_zone_domains (proxy_route_id);
CREATE INDEX IF NOT EXISTS idx_of_zone_domains_zone_id ON of_zone_domains (zone_id);
CREATE TABLE IF NOT EXISTS of_zones (
id BIGSERIAL PRIMARY KEY,
domain VARCHAR(255) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_of_zones_domain ON of_zones (domain);
INSERT INTO w_schedules (id, name, task_type, cron, payload, is_active, created_at, updated_at)
VALUES
(101, 'OpenFlare SSL 自动续期', 'of_ssl_renew', '0 0 * * *', '{}', TRUE, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
(103, 'OpenFlare WAF IP 组同步', 'of_waf_ip_group_sync', '*/5 * * * *', '{}', TRUE, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
(104, 'OpenFlare Uptime Kuma 同步', 'of_uptime_kuma_sync', '* * * * *', '{}', TRUE, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
ON CONFLICT (id) DO NOTHING;
INSERT INTO w_schedules (name, task_type, cron, payload, is_active, created_at, updated_at)
SELECT 'OpenFlare Pages 部署源扫描', 'of_pages_source_scan', '0 0 * * *', '{}', TRUE, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
WHERE NOT EXISTS (SELECT 1 FROM w_schedules WHERE task_type = 'of_pages_source_scan');
INSERT INTO w_system_configs (key, value, type, visibility, description, created_at, updated_at)
VALUES
('agent_heartbeat_interval', '3000', 'business', 0, 'Agent 心跳间隔(毫秒)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('agent_update_repo', 'Rain-kl/OpenFlare', 'business', 0, 'Agent 更新仓库', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('agent_websocket_upgrade_enabled', 'true', 'business', 0, 'Agent WebSocket 升级开关', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('geoip_provider', 'ipinfo', 'business', 0, 'GeoIP 服务商', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('log_retention_days_clickhouse', '30', 'business', 0, 'ClickHouse 日志保留天数', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('log_retention_days_postgres', '30', 'business', 0, 'PostgreSQL 日志保留天数(访问日志与可观测统一)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('log_retention_days_sqlite', '30', 'business', 0, 'SQLite 日志保留天数', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('metric_retention_days', '3', 'business', 0, '性能指标(CPU/内存/磁盘/网络)保留天数(三库共用)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('node_offline_threshold', '60000', 'business', 0, '节点离线阈值(毫秒)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_cache_enabled', 'false', 'business', 0, '缓存开关', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_cache_inactive', '30m', 'business', 0, '缓存不活跃时间', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_cache_key_template', '$scheme$host$request_uri', 'business', 0, '缓存键模板', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_cache_levels', '1:2', 'business', 0, '缓存层级', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_cache_lock_enabled', 'true', 'business', 0, '缓存锁开关', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_cache_lock_timeout', '5s', 'business', 0, '缓存锁超时', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_cache_max_size', '1g', 'business', 0, '缓存最大大小', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_cache_use_stale', 'error timeout updating http_500 http_502 http_503 http_504', 'business', 0, '缓存失效策略', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_client_body_timeout', '15', 'business', 0, '客户端体超时(秒)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_client_header_timeout', '15', 'business', 0, '客户端头超时(秒)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_client_max_body_size', '64m', 'business', 0, '客户端最大体大小', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_default_limit_conn_per_ip', '0', 'business', 0, '默认单 IP 并发连接上限(0 关闭)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_default_limit_conn_per_server', '0', 'business', 0, '默认站点并发连接上限(0 关闭)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_default_limit_rate', '', 'business', 0, '默认单请求带宽限速(空关闭)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_default_limit_req_per_ip', '', 'business', 0, '默认单 IP 请求频率限制(空关闭,例如 10r/s、100r/m)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_default_server_return_status', '421', 'business', 0, '默认服务器返回状态码', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_events_multi_accept_enabled', 'true', 'business', 0, '多路接受开关', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_events_use', 'epoll', 'business', 0, '事件模型', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_gzip_comp_level', '5', 'business', 0, 'Gzip 压缩级别', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_gzip_enabled', 'true', 'business', 0, 'Gzip 压缩开关', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_gzip_min_length', '1024', 'business', 0, 'Gzip 最小长度', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_http3_enabled', 'true', 'business', 0, 'HTTP/3 支持开关', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_keepalive_requests', '1000', 'business', 0, 'Keepalive 请求数', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_keepalive_timeout', '20', 'business', 0, 'Keepalive 超时(秒)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_large_client_header_buffers', '4 16k', 'business', 0, '大客户端头缓冲区', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_proxy_buffer_size', '8k', 'business', 0, '代理缓冲区大小', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_proxy_buffering_enabled', 'true', 'business', 0, '代理响应缓冲开关', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_proxy_buffers', '16 16k', 'business', 0, '代理缓冲区', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_proxy_busy_buffers_size', '64k', 'business', 0, '代理繁忙缓冲区大小', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_proxy_connect_timeout', '3', 'business', 0, '代理连接超时(秒)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_proxy_read_timeout', '60', 'business', 0, '代理读取超时(秒)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_proxy_request_buffering_enabled', 'false', 'business', 0, '代理请求缓冲开关', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_proxy_send_timeout', '60', 'business', 0, '代理发送超时(秒)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_send_timeout', '30', 'business', 0, '发送超时(秒)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_websocket_enabled', 'true', 'business', 0, 'WebSocket 支持开关', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_worker_connections', '4096', 'business', 0, 'Worker 连接数', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_worker_processes', 'auto', 'business', 0, 'Worker 进程数', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_worker_rlimit_nofile', '65535', 'business', 0, 'Worker 文件描述符限制', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('origin_error_page_enabled', 'true', 'business', 0, '是否启用源站错误页', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('origin_error_page_get_only', 'false', 'business', 0, '源站错误页是否仅对 GET 请求生效(其它方法透传)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('origin_error_page_html', '', 'business', 0, '源站错误页自定义 HTML,空则使用内置默认', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('origin_error_page_status_codes', '["500-599"]', 'business', 0, '源站错误页触发状态码标签 JSON 数组', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('pages_max_history_count', '20', 'business', 0, 'Pages 每个项目最大历史部署保留数(0 表示不限制)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('pages_max_package_size_mb', '100', 'business', 0, 'Pages 部署包上传大小上限(MiB)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('relay_frps_web_ui_enabled', 'false', 'business', 0, '是否启用 FRPS 内置 Web 管理界面', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('relay_frps_web_ui_port', '17500', 'business', 0, 'FRPS 内置 Web 管理界面端口', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('sw_offline_domains', '[]', 'business', 0, 'SW 离线兜底生效域名列表(JSON 数组,空则仅总开关无效)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('sw_offline_enabled', 'false', 'business', 0, '是否启用 Service Worker 离线兜底', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('sw_offline_html', '', 'business', 0, '离线联系页自定义 HTML,空则使用内置默认', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('uptime_kuma_enabled', 'false', 'business', 0, 'UptimeKuma 集成开关', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('uptime_kuma_interval', '60', 'business', 0, 'UptimeKuma 监控间隔(秒)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('uptime_kuma_monitor_scope', 'all', 'business', 0, 'UptimeKuma 监控范围', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('uptime_kuma_retry', '0', 'business', 0, 'UptimeKuma 重试次数', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('uptime_kuma_retry_interval', '60', 'business', 0, 'UptimeKuma 重试间隔(秒)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('uptime_kuma_sync_interval', '5', 'business', 0, 'UptimeKuma 同步间隔(分钟)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('uptime_kuma_timeout', '48', 'business', 0, 'UptimeKuma 超时(秒)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
ON CONFLICT (key) DO NOTHING;
-- +goose Down
SELECT 1;
@@ -0,0 +1,136 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package migrate
import (
"os"
"path/filepath"
"strings"
"testing"
)
var ofScheduleSeeds = []string{
"of_ssl_renew",
"of_waf_ip_group_sync",
"of_uptime_kuma_sync",
"of_pages_source_scan",
}
// ofConfigSeedKeys is G \ W from a gold v3.5.4 empty-DB goose run minus Wavelet admin 00001 keys.
var ofConfigSeedKeys = []string{
"agent_heartbeat_interval",
"agent_update_repo",
"agent_websocket_upgrade_enabled",
"geoip_provider",
"log_retention_days_clickhouse",
"log_retention_days_postgres",
"log_retention_days_sqlite",
"metric_retention_days",
"node_offline_threshold",
"openresty_cache_enabled",
"openresty_cache_inactive",
"openresty_cache_key_template",
"openresty_cache_levels",
"openresty_cache_lock_enabled",
"openresty_cache_lock_timeout",
"openresty_cache_max_size",
"openresty_cache_use_stale",
"openresty_client_body_timeout",
"openresty_client_header_timeout",
"openresty_client_max_body_size",
"openresty_default_limit_conn_per_ip",
"openresty_default_limit_conn_per_server",
"openresty_default_limit_rate",
"openresty_default_limit_req_per_ip",
"openresty_default_server_return_status",
"openresty_events_multi_accept_enabled",
"openresty_events_use",
"openresty_gzip_comp_level",
"openresty_gzip_enabled",
"openresty_gzip_min_length",
"openresty_http3_enabled",
"openresty_keepalive_requests",
"openresty_keepalive_timeout",
"openresty_large_client_header_buffers",
"openresty_proxy_buffer_size",
"openresty_proxy_buffering_enabled",
"openresty_proxy_buffers",
"openresty_proxy_busy_buffers_size",
"openresty_proxy_connect_timeout",
"openresty_proxy_read_timeout",
"openresty_proxy_request_buffering_enabled",
"openresty_proxy_send_timeout",
"openresty_send_timeout",
"openresty_websocket_enabled",
"openresty_worker_connections",
"openresty_worker_processes",
"openresty_worker_rlimit_nofile",
"origin_error_page_enabled",
"origin_error_page_get_only",
"origin_error_page_html",
"origin_error_page_status_codes",
"pages_max_history_count",
"pages_max_package_size_mb",
"relay_frps_web_ui_enabled",
"relay_frps_web_ui_port",
"sw_offline_domains",
"sw_offline_enabled",
"sw_offline_html",
"uptime_kuma_enabled",
"uptime_kuma_interval",
"uptime_kuma_monitor_scope",
"uptime_kuma_retry",
"uptime_kuma_retry_interval",
"uptime_kuma_sync_interval",
"uptime_kuma_timeout",
}
var waveletOwnedKeys = []string{
"cap_login_enabled",
"smtp_host",
"storage_config",
"site_name",
}
func TestInitialSQLContainsScheduleSeeds(t *testing.T) {
for _, dialect := range []string{"sqlite", "postgres"} {
s := readInitialSQL(t, dialect)
for _, needle := range ofScheduleSeeds {
if !strings.Contains(s, needle) {
t.Errorf("%s 00001 missing schedule seed %s", dialect, needle)
}
}
if strings.Contains(s, "of_database_auto_cleanup") {
t.Errorf("%s 00001 must not seed of_database_auto_cleanup", dialect)
}
}
}
func TestInitialSQLContainsConfigSeedDiff(t *testing.T) {
for _, dialect := range []string{"sqlite", "postgres"} {
s := readInitialSQL(t, dialect)
if !strings.Contains(s, "INSERT") {
t.Errorf("%s 00001 has no INSERT", dialect)
}
for _, key := range ofConfigSeedKeys {
if !strings.Contains(s, "'"+key+"'") {
t.Errorf("%s 00001 missing config seed %s", dialect, key)
}
}
for _, key := range waveletOwnedKeys {
if strings.Contains(s, "'"+key+"'") {
t.Errorf("%s 00001 must not seed Wavelet key %s", dialect, key)
}
}
}
}
func readInitialSQL(t *testing.T, dialect string) string {
t.Helper()
b, err := os.ReadFile(filepath.Join(dialect, "00001_initial.sql"))
if err != nil {
t.Fatal(err)
}
return string(b)
}
@@ -0,0 +1,563 @@
-- +goose Up
CREATE TABLE IF NOT EXISTS of_acme_accounts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL DEFAULT '',
url TEXT NOT NULL DEFAULT '',
private_key TEXT NOT NULL DEFAULT '',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS of_apply_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
node_id TEXT NOT NULL,
version TEXT NOT NULL,
result TEXT NOT NULL,
message TEXT,
checksum TEXT NOT NULL DEFAULT '',
main_config_checksum TEXT NOT NULL DEFAULT '',
route_config_checksum TEXT NOT NULL DEFAULT '',
support_file_count INTEGER NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_of_apply_logs_created_at ON of_apply_logs(created_at);
CREATE INDEX IF NOT EXISTS idx_of_apply_logs_node_id ON of_apply_logs(node_id);
CREATE TABLE IF NOT EXISTS of_cf_connections (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source TEXT NOT NULL DEFAULT '',
dns_account_id INTEGER,
authorization TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT '',
verified_at DATETIME,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_of_cf_connections_dns_account_id ON of_cf_connections (dns_account_id);
CREATE TABLE IF NOT EXISTS of_cf_pointing_groups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
primary_node_id INTEGER NOT NULL,
backup_node_id INTEGER,
active_node_id INTEGER NOT NULL,
default_proxied BOOLEAN NOT NULL DEFAULT FALSE,
enabled BOOLEAN NOT NULL DEFAULT FALSE,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_of_cf_pointing_groups_active_node_id ON of_cf_pointing_groups (active_node_id);
CREATE INDEX IF NOT EXISTS idx_of_cf_pointing_groups_backup_node_id ON of_cf_pointing_groups (backup_node_id);
CREATE INDEX IF NOT EXISTS idx_of_cf_pointing_groups_primary_node_id ON of_cf_pointing_groups (primary_node_id);
CREATE TABLE IF NOT EXISTS of_cf_pointing_members (
id INTEGER PRIMARY KEY AUTOINCREMENT,
group_id INTEGER NOT NULL,
zone_domain_id INTEGER NOT NULL,
proxied BOOLEAN NOT NULL DEFAULT FALSE,
cf_zone_id TEXT NOT NULL DEFAULT '',
cf_record_id TEXT NOT NULL DEFAULT '',
desired_ip TEXT NOT NULL DEFAULT '',
sync_status TEXT NOT NULL DEFAULT 'pending',
last_error TEXT NOT NULL DEFAULT '',
synced_at DATETIME,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_of_cf_pointing_members_group_id ON of_cf_pointing_members (group_id);
CREATE UNIQUE INDEX IF NOT EXISTS idx_of_cf_pointing_members_zone_domain_id ON of_cf_pointing_members (zone_domain_id);
CREATE TABLE IF NOT EXISTS of_config_versions (
version VARCHAR(32) PRIMARY KEY,
snapshot_json TEXT NOT NULL,
main_config TEXT NOT NULL DEFAULT '',
rendered_config TEXT NOT NULL,
support_files_json TEXT NOT NULL DEFAULT '[]',
checksum VARCHAR(64) NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT FALSE,
created_by VARCHAR(64) NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_of_config_versions_is_active ON of_config_versions (is_active);
CREATE TABLE IF NOT EXISTS of_dns_accounts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
type TEXT NOT NULL,
authorization TEXT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS of_node_access_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
node_id TEXT NOT NULL DEFAULT '',
logged_at DATETIME NOT NULL,
remote_addr TEXT NOT NULL DEFAULT '',
region TEXT NOT NULL DEFAULT '',
host TEXT NOT NULL DEFAULT '',
path TEXT NOT NULL DEFAULT '',
user_agent TEXT NOT NULL DEFAULT '',
cache_status TEXT NOT NULL DEFAULT '',
status_code INTEGER NOT NULL DEFAULT 0,
bytes_sent INTEGER NOT NULL DEFAULT 0,
request_length INTEGER NOT NULL DEFAULT 0,
request_time_ms INTEGER NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_of_node_access_logs_host ON of_node_access_logs (host, logged_at DESC);
CREATE INDEX IF NOT EXISTS idx_of_node_access_logs_host_lower ON of_node_access_logs (lower(trim(host)));
CREATE INDEX IF NOT EXISTS idx_of_node_access_logs_logged_at ON of_node_access_logs (logged_at DESC, id DESC);
CREATE INDEX IF NOT EXISTS idx_of_node_access_logs_node_id ON of_node_access_logs (node_id, logged_at DESC);
CREATE INDEX IF NOT EXISTS idx_of_node_access_logs_remote_addr ON of_node_access_logs (remote_addr, logged_at DESC);
CREATE INDEX IF NOT EXISTS idx_of_node_access_logs_status_code ON of_node_access_logs (status_code, logged_at DESC);
CREATE TABLE IF NOT EXISTS of_node_edge_health (
id INTEGER PRIMARY KEY AUTOINCREMENT,
node_id TEXT NOT NULL DEFAULT '',
captured_at DATETIME NOT NULL,
status TEXT NOT NULL DEFAULT '',
connections INTEGER NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_of_node_edge_health_node ON of_node_edge_health (node_id, captured_at DESC);
CREATE TABLE IF NOT EXISTS of_node_health_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
node_id TEXT NOT NULL,
event_type TEXT NOT NULL,
severity TEXT NOT NULL,
status TEXT NOT NULL,
message TEXT,
first_triggered_at DATETIME NOT NULL,
last_triggered_at DATETIME NOT NULL,
reported_at DATETIME NOT NULL,
resolved_at DATETIME,
metadata_json TEXT,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_of_node_health_events_event_type ON of_node_health_events (event_type);
CREATE INDEX IF NOT EXISTS idx_of_node_health_events_first_triggered_at ON of_node_health_events (first_triggered_at);
CREATE INDEX IF NOT EXISTS idx_of_node_health_events_last_triggered_at ON of_node_health_events (last_triggered_at);
CREATE INDEX IF NOT EXISTS idx_of_node_health_events_node_id ON of_node_health_events (node_id);
CREATE INDEX IF NOT EXISTS idx_of_node_health_events_reported_at ON of_node_health_events (reported_at);
CREATE INDEX IF NOT EXISTS idx_of_node_health_events_resolved_at ON of_node_health_events (resolved_at);
CREATE INDEX IF NOT EXISTS idx_of_node_health_events_status ON of_node_health_events (status);
CREATE TABLE IF NOT EXISTS of_node_metric_snapshots (
id INTEGER PRIMARY KEY AUTOINCREMENT,
node_id TEXT NOT NULL DEFAULT '',
captured_at DATETIME NOT NULL,
cpu_usage_percent REAL NOT NULL DEFAULT 0,
memory_used_bytes INTEGER NOT NULL DEFAULT 0,
memory_total_bytes INTEGER NOT NULL DEFAULT 0,
storage_used_bytes INTEGER NOT NULL DEFAULT 0,
storage_total_bytes INTEGER NOT NULL DEFAULT 0,
disk_read_bytes INTEGER NOT NULL DEFAULT 0,
disk_write_bytes INTEGER NOT NULL DEFAULT 0,
network_rx_bytes INTEGER NOT NULL DEFAULT 0,
network_tx_bytes INTEGER NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_of_node_metric_snapshots_node ON of_node_metric_snapshots (node_id, captured_at DESC);
CREATE TABLE IF NOT EXISTS of_node_obs_frpc (
id INTEGER PRIMARY KEY AUTOINCREMENT,
node_id TEXT NOT NULL DEFAULT '',
captured_at DATETIME NOT NULL,
tunnel_status TEXT NOT NULL DEFAULT '',
connected_relays_count INTEGER NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_of_node_obs_frpc_node ON of_node_obs_frpc (node_id, captured_at DESC);
CREATE TABLE IF NOT EXISTS of_node_obs_frps (
id INTEGER PRIMARY KEY AUTOINCREMENT,
node_id TEXT NOT NULL DEFAULT '',
captured_at DATETIME NOT NULL,
frps_connections INTEGER NOT NULL DEFAULT 0,
frps_proxy_count INTEGER NOT NULL DEFAULT 0,
frps_client_count INTEGER NOT NULL DEFAULT 0,
frps_proxies TEXT NOT NULL DEFAULT '',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_of_node_obs_frps_node ON of_node_obs_frps (node_id, captured_at DESC);
CREATE TABLE IF NOT EXISTS of_node_system_profiles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
node_id TEXT NOT NULL,
hostname TEXT NOT NULL DEFAULT '',
os_name TEXT NOT NULL DEFAULT '',
os_version TEXT NOT NULL DEFAULT '',
kernel_version TEXT NOT NULL DEFAULT '',
architecture TEXT NOT NULL DEFAULT '',
cpu_model TEXT NOT NULL DEFAULT '',
cpu_cores INTEGER NOT NULL DEFAULT 0,
total_memory_bytes INTEGER NOT NULL DEFAULT 0,
total_disk_bytes INTEGER NOT NULL DEFAULT 0,
uptime_seconds INTEGER NOT NULL DEFAULT 0,
reported_at DATETIME NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_of_node_system_profiles_node_id ON of_node_system_profiles (node_id);
CREATE INDEX IF NOT EXISTS idx_of_node_system_profiles_reported_at ON of_node_system_profiles (reported_at);
CREATE TABLE IF NOT EXISTS of_nodes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
node_id TEXT NOT NULL,
name TEXT NOT NULL,
ip TEXT NOT NULL DEFAULT '',
ip_manual_override INTEGER NOT NULL DEFAULT 0,
geo_name TEXT NOT NULL DEFAULT '',
geo_latitude REAL,
geo_longitude REAL,
geo_manual_override INTEGER NOT NULL DEFAULT 0,
access_token TEXT NOT NULL DEFAULT '',
auto_update_enabled INTEGER NOT NULL DEFAULT 0,
update_requested INTEGER NOT NULL DEFAULT 0,
update_channel TEXT NOT NULL DEFAULT 'stable',
update_tag TEXT NOT NULL DEFAULT '',
restart_openresty_requested INTEGER NOT NULL DEFAULT 0,
version TEXT NOT NULL DEFAULT '',
ext_version TEXT NOT NULL DEFAULT '',
openresty_status TEXT NOT NULL DEFAULT 'unknown',
openresty_message TEXT,
status TEXT NOT NULL DEFAULT 'offline',
current_version TEXT NOT NULL DEFAULT '',
last_seen_at DATETIME,
last_error TEXT,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
node_type TEXT NOT NULL DEFAULT 'edge_node',
relay_bind_port INTEGER NOT NULL DEFAULT 0,
relay_vhost_http_port INTEGER NOT NULL DEFAULT 0,
relay_auth_token TEXT NOT NULL DEFAULT '',
relay_agent_access_addr TEXT NOT NULL DEFAULT '',
relay_client_access_addr TEXT NOT NULL DEFAULT '',
relay_client_proxy_url TEXT NOT NULL DEFAULT '',
capabilities_json TEXT NOT NULL DEFAULT '[]',
relay_status TEXT NOT NULL DEFAULT 'unknown',
relay_web_server_enabled INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_of_nodes_access_token ON of_nodes (access_token);
CREATE UNIQUE INDEX IF NOT EXISTS idx_of_nodes_node_id ON of_nodes (node_id);
CREATE TABLE IF NOT EXISTS of_origins (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
address TEXT NOT NULL,
remark TEXT NOT NULL DEFAULT '',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_of_origins_address ON of_origins (address);
CREATE TABLE IF NOT EXISTS of_pages_deployment_files (
id INTEGER PRIMARY KEY AUTOINCREMENT,
deployment_id INTEGER NOT NULL,
path TEXT NOT NULL,
size INTEGER NOT NULL DEFAULT 0,
checksum TEXT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_of_pages_deployment_files_deployment_id ON of_pages_deployment_files (deployment_id);
CREATE TABLE IF NOT EXISTS of_pages_deployments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id INTEGER NOT NULL,
deployment_number INTEGER NOT NULL,
checksum TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'uploaded',
artifact_path TEXT NOT NULL DEFAULT '',
file_count INTEGER NOT NULL DEFAULT 0,
total_size INTEGER NOT NULL DEFAULT 0,
created_by TEXT NOT NULL DEFAULT '',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
activated_at DATETIME,
upload_id INTEGER NOT NULL DEFAULT 0,
source_type TEXT NOT NULL DEFAULT '',
source_identity TEXT,
source_revision TEXT,
source_label TEXT NOT NULL DEFAULT '',
source_meta TEXT NOT NULL DEFAULT '',
trigger_type TEXT NOT NULL DEFAULT ''
);
CREATE INDEX IF NOT EXISTS idx_of_pages_deployments_checksum ON of_pages_deployments (checksum);
CREATE INDEX IF NOT EXISTS idx_of_pages_deployments_project_id ON of_pages_deployments (project_id);
CREATE UNIQUE INDEX IF NOT EXISTS idx_of_pages_deployments_project_number
ON of_pages_deployments (project_id, deployment_number);
CREATE UNIQUE INDEX IF NOT EXISTS idx_of_pages_deployments_source_revision
ON of_pages_deployments (project_id, source_identity, source_revision)
WHERE source_identity IS NOT NULL AND source_revision IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_of_pages_deployments_status ON of_pages_deployments (status);
CREATE INDEX IF NOT EXISTS idx_of_pages_deployments_upload_id ON of_pages_deployments (upload_id);
CREATE TABLE IF NOT EXISTS of_pages_project_source_runtime (
source_id INTEGER PRIMARY KEY,
etag TEXT NOT NULL DEFAULT '',
last_seen_revision TEXT NOT NULL DEFAULT '',
last_seen_detail TEXT NOT NULL DEFAULT '',
last_applied_revision TEXT NOT NULL DEFAULT '',
last_applied_detail TEXT NOT NULL DEFAULT '',
sync_status TEXT NOT NULL DEFAULT '',
last_error TEXT NOT NULL DEFAULT '',
last_checked_at DATETIME,
last_synced_at DATETIME,
next_check_at DATETIME,
lease_expires_at DATETIME,
lease_token TEXT NOT NULL DEFAULT '',
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_of_pages_project_source_runtime_next_check_at
ON of_pages_project_source_runtime (next_check_at);
CREATE TABLE IF NOT EXISTS of_pages_project_sources (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id INTEGER NOT NULL,
source_type TEXT NOT NULL DEFAULT '',
remote_url TEXT NOT NULL DEFAULT '',
allow_insecure INTEGER NOT NULL DEFAULT 0,
github_repository TEXT NOT NULL DEFAULT '',
release_selector TEXT NOT NULL DEFAULT '',
release_tag TEXT NOT NULL DEFAULT '',
asset_name TEXT NOT NULL DEFAULT '',
auto_update_enabled INTEGER NOT NULL DEFAULT 0,
check_interval_minutes INTEGER NOT NULL DEFAULT 0,
config_version INTEGER NOT NULL DEFAULT 0,
source_identity TEXT NOT NULL DEFAULT '',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_of_pages_project_sources_project_id
ON of_pages_project_sources (project_id);
CREATE TABLE IF NOT EXISTS of_pages_projects (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
slug TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
enabled INTEGER NOT NULL DEFAULT 1,
spa_fallback_enabled INTEGER NOT NULL DEFAULT 0,
spa_fallback_path TEXT NOT NULL DEFAULT '/index.html',
api_proxy_enabled INTEGER NOT NULL DEFAULT 0,
api_proxy_path TEXT NOT NULL DEFAULT '',
api_proxy_pass TEXT NOT NULL DEFAULT '',
api_proxy_rewrite TEXT NOT NULL DEFAULT '',
active_deployment_id INTEGER,
root_dir TEXT NOT NULL DEFAULT '',
entry_file TEXT NOT NULL DEFAULT 'index.html',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
content_config_version INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_of_pages_projects_active_deployment_id ON of_pages_projects (active_deployment_id);
CREATE UNIQUE INDEX IF NOT EXISTS idx_of_pages_projects_slug ON of_pages_projects (slug);
CREATE TABLE IF NOT EXISTS of_proxy_routes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
site_name TEXT NOT NULL DEFAULT '',
origin_id INTEGER,
origin_url TEXT NOT NULL,
origin_host TEXT NOT NULL DEFAULT '',
upstreams TEXT NOT NULL DEFAULT '[]',
enabled INTEGER NOT NULL DEFAULT 1,
enable_https INTEGER NOT NULL DEFAULT 0,
redirect_http INTEGER NOT NULL DEFAULT 0,
limit_conn_per_server INTEGER NOT NULL DEFAULT 0,
limit_conn_per_ip INTEGER NOT NULL DEFAULT 0,
limit_rate TEXT NOT NULL DEFAULT '',
cache_enabled INTEGER NOT NULL DEFAULT 0,
cache_policy TEXT NOT NULL DEFAULT '',
cache_rules TEXT NOT NULL DEFAULT '[]',
custom_headers TEXT NOT NULL DEFAULT '[]',
basic_auth_enabled INTEGER NOT NULL DEFAULT 0,
basic_auth_username TEXT NOT NULL DEFAULT '',
basic_auth_password TEXT NOT NULL DEFAULT '',
upstream_type TEXT NOT NULL DEFAULT 'direct',
tunnel_node_id INTEGER,
tunnel_target_addr TEXT NOT NULL DEFAULT '',
tunnel_target_protocol TEXT NOT NULL DEFAULT '',
pages_project_id INTEGER,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
limit_req_per_ip VARCHAR(32) NOT NULL DEFAULT ''
);
CREATE INDEX IF NOT EXISTS idx_of_proxy_routes_origin_id ON of_proxy_routes (origin_id);
CREATE INDEX IF NOT EXISTS idx_of_proxy_routes_pages_project_id ON of_proxy_routes (pages_project_id);
CREATE UNIQUE INDEX IF NOT EXISTS idx_of_proxy_routes_site_name ON of_proxy_routes (site_name);
CREATE INDEX IF NOT EXISTS idx_of_proxy_routes_tunnel_node_id ON of_proxy_routes (tunnel_node_id);
CREATE TABLE IF NOT EXISTS of_tls_certificates (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
cert_pem TEXT NOT NULL,
key_pem TEXT NOT NULL,
not_before DATETIME,
not_after DATETIME,
remark TEXT NOT NULL DEFAULT '',
provider TEXT NOT NULL DEFAULT 'upload',
acme_account_id INTEGER NOT NULL DEFAULT 0,
dns_account_id INTEGER NOT NULL DEFAULT 0,
key_algorithm TEXT NOT NULL DEFAULT '',
auto_renew INTEGER NOT NULL DEFAULT 0,
primary_domain TEXT NOT NULL DEFAULT '',
other_domains TEXT NOT NULL DEFAULT '',
disable_cname INTEGER NOT NULL DEFAULT 0,
skip_dns INTEGER NOT NULL DEFAULT 0,
dns1 TEXT NOT NULL DEFAULT '',
dns2 TEXT NOT NULL DEFAULT '',
apply_status TEXT NOT NULL DEFAULT 'ready',
apply_message TEXT NOT NULL DEFAULT '',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_of_tls_certificates_name ON of_tls_certificates (name);
CREATE TABLE IF NOT EXISTS of_waf_ip_groups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
type TEXT NOT NULL,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
ip_list TEXT NOT NULL DEFAULT '[]',
auto_config TEXT NOT NULL DEFAULT '{}',
ext_ips TEXT NOT NULL DEFAULT '[]',
subscription_url TEXT NOT NULL DEFAULT '',
subscription_format TEXT NOT NULL DEFAULT 'text',
subscription_mapping_rule TEXT NOT NULL DEFAULT '',
sync_interval_minutes INTEGER NOT NULL DEFAULT 1440,
last_synced_at DATETIME,
next_sync_at DATETIME,
last_sync_status TEXT NOT NULL DEFAULT '',
last_sync_message TEXT NOT NULL DEFAULT '',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_of_waf_ip_groups_next_sync_at ON of_waf_ip_groups (next_sync_at);
CREATE INDEX IF NOT EXISTS idx_of_waf_ip_groups_type ON of_waf_ip_groups (type);
CREATE TABLE IF NOT EXISTS of_waf_rule_group_bindings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
rule_group_id INTEGER NOT NULL,
proxy_route_id INTEGER NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
sequence INTEGER NOT NULL DEFAULT 0
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_of_waf_group_route ON of_waf_rule_group_bindings (rule_group_id, proxy_route_id);
CREATE INDEX IF NOT EXISTS idx_of_waf_rule_group_bindings_proxy_route_id ON of_waf_rule_group_bindings (proxy_route_id);
CREATE TABLE IF NOT EXISTS of_waf_rule_groups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
is_global BOOLEAN NOT NULL DEFAULT FALSE,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
graph TEXT NOT NULL DEFAULT '',
revision INTEGER NOT NULL DEFAULT 1
);
CREATE INDEX IF NOT EXISTS idx_of_waf_rule_groups_is_global ON of_waf_rule_groups (is_global);
CREATE TABLE IF NOT EXISTS of_zone_domains (
id INTEGER PRIMARY KEY AUTOINCREMENT,
zone_id INTEGER NOT NULL,
proxy_route_id INTEGER,
domain TEXT NOT NULL,
cert_id INTEGER,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_of_zone_domains_cert_id ON of_zone_domains (cert_id);
CREATE UNIQUE INDEX IF NOT EXISTS idx_of_zone_domains_domain ON of_zone_domains (domain);
CREATE INDEX IF NOT EXISTS idx_of_zone_domains_proxy_route_id ON of_zone_domains (proxy_route_id);
CREATE INDEX IF NOT EXISTS idx_of_zone_domains_zone_id ON of_zone_domains (zone_id);
CREATE TABLE IF NOT EXISTS of_zones (
id INTEGER PRIMARY KEY AUTOINCREMENT,
domain TEXT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_of_zones_domain ON of_zones (domain);
INSERT OR IGNORE INTO w_schedules (id, name, task_type, cron, payload, is_active, created_at, updated_at)
VALUES
(101, 'OpenFlare SSL 自动续期', 'of_ssl_renew', '0 0 * * *', '{}', 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
(103, 'OpenFlare WAF IP 组同步', 'of_waf_ip_group_sync', '*/5 * * * *', '{}', 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
(104, 'OpenFlare Uptime Kuma 同步', 'of_uptime_kuma_sync', '* * * * *', '{}', 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);
INSERT INTO w_schedules (name, task_type, cron, payload, is_active, created_at, updated_at)
SELECT 'OpenFlare Pages 部署源扫描', 'of_pages_source_scan', '0 0 * * *', '{}', 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
WHERE NOT EXISTS (SELECT 1 FROM w_schedules WHERE task_type = 'of_pages_source_scan');
INSERT OR IGNORE INTO w_system_configs (key, value, type, visibility, description, created_at, updated_at)
VALUES
('agent_heartbeat_interval', '3000', 'business', 0, 'Agent 心跳间隔(毫秒)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('agent_update_repo', 'Rain-kl/OpenFlare', 'business', 0, 'Agent 更新仓库', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('agent_websocket_upgrade_enabled', 'true', 'business', 0, 'Agent WebSocket 升级开关', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('geoip_provider', 'ipinfo', 'business', 0, 'GeoIP 服务商', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('log_retention_days_clickhouse', '30', 'business', 0, 'ClickHouse 日志保留天数', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('log_retention_days_postgres', '30', 'business', 0, 'PostgreSQL 日志保留天数(访问日志与可观测统一)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('log_retention_days_sqlite', '30', 'business', 0, 'SQLite 日志保留天数', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('metric_retention_days', '3', 'business', 0, '性能指标(CPU/内存/磁盘/网络)保留天数(三库共用)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('node_offline_threshold', '60000', 'business', 0, '节点离线阈值(毫秒)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_cache_enabled', 'false', 'business', 0, '缓存开关', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_cache_inactive', '30m', 'business', 0, '缓存不活跃时间', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_cache_key_template', '$scheme$host$request_uri', 'business', 0, '缓存键模板', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_cache_levels', '1:2', 'business', 0, '缓存层级', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_cache_lock_enabled', 'true', 'business', 0, '缓存锁开关', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_cache_lock_timeout', '5s', 'business', 0, '缓存锁超时', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_cache_max_size', '1g', 'business', 0, '缓存最大大小', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_cache_use_stale', 'error timeout updating http_500 http_502 http_503 http_504', 'business', 0, '缓存失效策略', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_client_body_timeout', '15', 'business', 0, '客户端体超时(秒)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_client_header_timeout', '15', 'business', 0, '客户端头超时(秒)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_client_max_body_size', '64m', 'business', 0, '客户端最大体大小', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_default_limit_conn_per_ip', '0', 'business', 0, '默认单 IP 并发连接上限(0 关闭)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_default_limit_conn_per_server', '0', 'business', 0, '默认站点并发连接上限(0 关闭)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_default_limit_rate', '', 'business', 0, '默认单请求带宽限速(空关闭)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_default_limit_req_per_ip', '', 'business', 0, '默认单 IP 请求频率限制(空关闭,例如 10r/s、100r/m)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_default_server_return_status', '421', 'business', 0, '默认服务器返回状态码', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_events_multi_accept_enabled', 'true', 'business', 0, '多路接受开关', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_events_use', 'epoll', 'business', 0, '事件模型', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_gzip_comp_level', '5', 'business', 0, 'Gzip 压缩级别', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_gzip_enabled', 'true', 'business', 0, 'Gzip 压缩开关', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_gzip_min_length', '1024', 'business', 0, 'Gzip 最小长度', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_http3_enabled', 'true', 'business', 0, 'HTTP/3 支持开关', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_keepalive_requests', '1000', 'business', 0, 'Keepalive 请求数', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_keepalive_timeout', '20', 'business', 0, 'Keepalive 超时(秒)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_large_client_header_buffers', '4 16k', 'business', 0, '大客户端头缓冲区', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_proxy_buffer_size', '8k', 'business', 0, '代理缓冲区大小', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_proxy_buffering_enabled', 'true', 'business', 0, '代理响应缓冲开关', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_proxy_buffers', '16 16k', 'business', 0, '代理缓冲区', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_proxy_busy_buffers_size', '64k', 'business', 0, '代理繁忙缓冲区大小', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_proxy_connect_timeout', '3', 'business', 0, '代理连接超时(秒)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_proxy_read_timeout', '60', 'business', 0, '代理读取超时(秒)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_proxy_request_buffering_enabled', 'false', 'business', 0, '代理请求缓冲开关', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_proxy_send_timeout', '60', 'business', 0, '代理发送超时(秒)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_send_timeout', '30', 'business', 0, '发送超时(秒)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_websocket_enabled', 'true', 'business', 0, 'WebSocket 支持开关', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_worker_connections', '4096', 'business', 0, 'Worker 连接数', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_worker_processes', 'auto', 'business', 0, 'Worker 进程数', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('openresty_worker_rlimit_nofile', '65535', 'business', 0, 'Worker 文件描述符限制', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('origin_error_page_enabled', 'true', 'business', 0, '是否启用源站错误页', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('origin_error_page_get_only', 'false', 'business', 0, '源站错误页是否仅对 GET 请求生效(其它方法透传)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('origin_error_page_html', '', 'business', 0, '源站错误页自定义 HTML,空则使用内置默认', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('origin_error_page_status_codes', '["500-599"]', 'business', 0, '源站错误页触发状态码标签 JSON 数组', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('pages_max_history_count', '20', 'business', 0, 'Pages 每个项目最大历史部署保留数(0 表示不限制)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('pages_max_package_size_mb', '100', 'business', 0, 'Pages 部署包上传大小上限(MiB)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('relay_frps_web_ui_enabled', 'false', 'business', 0, '是否启用 FRPS 内置 Web 管理界面', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('relay_frps_web_ui_port', '17500', 'business', 0, 'FRPS 内置 Web 管理界面端口', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('sw_offline_domains', '[]', 'business', 0, 'SW 离线兜底生效域名列表(JSON 数组,空则仅总开关无效)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('sw_offline_enabled', 'false', 'business', 0, '是否启用 Service Worker 离线兜底', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('sw_offline_html', '', 'business', 0, '离线联系页自定义 HTML,空则使用内置默认', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('uptime_kuma_enabled', 'false', 'business', 0, 'UptimeKuma 集成开关', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('uptime_kuma_interval', '60', 'business', 0, 'UptimeKuma 监控间隔(秒)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('uptime_kuma_monitor_scope', 'all', 'business', 0, 'UptimeKuma 监控范围', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('uptime_kuma_retry', '0', 'business', 0, 'UptimeKuma 重试次数', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('uptime_kuma_retry_interval', '60', 'business', 0, 'UptimeKuma 重试间隔(秒)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('uptime_kuma_sync_interval', '5', 'business', 0, 'UptimeKuma 同步间隔(分钟)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('uptime_kuma_timeout', '48', 'business', 0, 'UptimeKuma 超时(秒)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);
-- +goose Down
SELECT 1;
@@ -0,0 +1,200 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
// Package migrate records pre-Cordis schema versions and applies of_* SQL.
package migrate
import (
"context"
"database/sql"
"fmt"
"unicode"
"Wavelet/core"
"Wavelet/core/contracts"
"Wavelet/openflare/plugins/server/domain/site/zone"
)
const (
legacyPluginID = "openflare/legacy"
serverPluginID = "server"
serverInitialVersion = int64(1)
zoneImportSQLVersion = int64(202607120002)
zoneDropLegacySQLVersion = int64(202607130001)
gooseVersionTable = "goose_db_version"
)
// Legacy copies goose_db_version into w_schema_versions so the 76-file mixed
// chain is not re-run. Fresh databases have no goose table and are left alone.
func Legacy(ctx *core.Context) error {
dbSvc, err := core.Inject[contracts.DBService](ctx)
if err != nil {
return fmt.Errorf("stamp: inject DBService: %w", err)
}
gormDB := dbSvc.GORM()
if gormDB == nil {
return fmt.Errorf("stamp: DBService.GORM() returned nil")
}
sqlDB, err := gormDB.DB()
if err != nil {
return fmt.Errorf("stamp: get sql.DB: %w", err)
}
goCtx := context.Background()
if ctx != nil && ctx.GoContext() != nil {
goCtx = ctx.GoContext()
}
postgres := gormDB.Dialector != nil && gormDB.Dialector.Name() == "postgres"
exists, err := gooseTableExists(goCtx, sqlDB, postgres)
if err != nil {
return err
}
if !exists {
return nil
}
col, err := gooseVersionColumn(goCtx, sqlDB, postgres)
if err != nil {
return err
}
maxVer, err := gooseMaxVersion(goCtx, sqlDB, col)
if err != nil {
return err
}
if err := insertStamps(goCtx, sqlDB, postgres, maxVer); err != nil {
return err
}
return maybeImportZones(goCtx, sqlDB, postgres, maxVer)
}
func gooseTableExists(ctx context.Context, db *sql.DB, postgres bool) (bool, error) {
var n int
var err error
if postgres {
err = db.QueryRowContext(ctx, `
SELECT COUNT(*) FROM information_schema.tables
WHERE table_schema = 'public' AND table_name = $1
`, gooseVersionTable).Scan(&n)
} else {
err = db.QueryRowContext(ctx,
`SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = ?`,
gooseVersionTable,
).Scan(&n)
}
if err != nil {
return false, fmt.Errorf("stamp: detect %s: %w", gooseVersionTable, err)
}
return n > 0, nil
}
func gooseVersionColumn(ctx context.Context, db *sql.DB, postgres bool) (string, error) {
var rows *sql.Rows
var err error
if postgres {
rows, err = db.QueryContext(ctx, `
SELECT column_name FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = $1
`, gooseVersionTable)
} else {
rows, err = db.QueryContext(ctx, `SELECT name FROM pragma_table_info(?)`, gooseVersionTable)
}
if err != nil {
return "", fmt.Errorf("stamp: list %s columns: %w", gooseVersionTable, err)
}
defer func() { _ = rows.Close() }()
var names []string
for rows.Next() {
var name string
if err := rows.Scan(&name); err != nil {
return "", fmt.Errorf("stamp: scan %s columns: %w", gooseVersionTable, err)
}
names = append(names, name)
}
if err := rows.Err(); err != nil {
return "", fmt.Errorf("stamp: list %s columns: %w", gooseVersionTable, err)
}
hasVersionID, hasVersion := false, false
for _, name := range names {
switch name {
case "version_id":
hasVersionID = true
case "version":
hasVersion = true
}
}
switch {
case hasVersionID:
return "version_id", nil
case hasVersion:
return "version", nil
default:
return "", fmt.Errorf("stamp: %s has no version_id or version column", gooseVersionTable)
}
}
func gooseMaxVersion(ctx context.Context, db *sql.DB, column string) (int64, error) {
if !safeIdent(column) {
return 0, fmt.Errorf("stamp: unsafe version column %q", column)
}
var maxVer int64
q := fmt.Sprintf("SELECT COALESCE(MAX(%s), 0) FROM %s", column, gooseVersionTable)
if err := db.QueryRowContext(ctx, q).Scan(&maxVer); err != nil {
return 0, fmt.Errorf("stamp: max %s: %w", gooseVersionTable, err)
}
return maxVer, nil
}
func insertStamps(ctx context.Context, db *sql.DB, postgres bool, maxVer int64) error {
q := `INSERT INTO w_schema_versions (plugin_id, version_id) VALUES (?, ?) ON CONFLICT (plugin_id, version_id) DO NOTHING`
if postgres {
q = `INSERT INTO w_schema_versions (plugin_id, version_id) VALUES ($1, $2) ON CONFLICT (plugin_id, version_id) DO NOTHING`
}
stamps := []struct {
plugin string
ver int64
}{
{legacyPluginID, 0},
{legacyPluginID, maxVer},
{serverPluginID, serverInitialVersion},
}
for _, s := range stamps {
if _, err := db.ExecContext(ctx, q, s.plugin, s.ver); err != nil {
return fmt.Errorf("stamp: insert (%s, %d): %w", s.plugin, s.ver, err)
}
}
return nil
}
func maybeImportZones(ctx context.Context, db *sql.DB, postgres bool, maxVer int64) error {
if maxVer < zoneImportSQLVersion || maxVer >= zoneDropLegacySQLVersion {
return nil
}
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("stamp: begin zone import: %w", err)
}
report, err := zone.ImportLegacyTx(ctx, tx, postgres)
if err != nil {
_ = tx.Rollback()
return report.LogAndReturn(err)
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("stamp: commit zone import: %w", err)
}
return nil
}
func safeIdent(name string) bool {
if name == "" {
return false
}
for _, r := range name {
if r != '_' && !unicode.IsLetter(r) && !unicode.IsDigit(r) {
return false
}
}
return true
}
@@ -0,0 +1,151 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package migrate
import (
"context"
"database/sql"
"path/filepath"
"testing"
"Wavelet/core"
"Wavelet/core/contracts"
"github.com/glebarez/sqlite"
"gorm.io/gorm"
)
const goldGooseVersion int64 = 202608090003
type testDB struct {
db *gorm.DB
}
func (s testDB) GORM() *gorm.DB { return s.db }
func (s testDB) DB(ctx context.Context) *gorm.DB { return s.db.WithContext(ctx) }
func (s testDB) Named(string) *gorm.DB { return s.db }
func openStampDB(t *testing.T) (*gorm.DB, *sql.DB) {
t.Helper()
gdb, err := gorm.Open(sqlite.Open(filepath.Join(t.TempDir(), "stamp.db")), &gorm.Config{})
if err != nil {
t.Fatalf("gorm.Open(sqlite) error = %v", err)
}
sqlDB, err := gdb.DB()
if err != nil {
t.Fatalf("gdb.DB() error = %v", err)
}
return gdb, sqlDB
}
func createSchemaVersions(t *testing.T, db *sql.DB) {
t.Helper()
_, err := db.Exec(`CREATE TABLE IF NOT EXISTS w_schema_versions (
plugin_id VARCHAR(64) NOT NULL,
version_id BIGINT NOT NULL,
applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (plugin_id, version_id)
)`)
if err != nil {
t.Fatalf("create w_schema_versions error = %v", err)
}
}
func createGooseDBVersion(t *testing.T, db *sql.DB, version int64) {
t.Helper()
_, err := db.Exec(`CREATE TABLE goose_db_version (
id INTEGER PRIMARY KEY AUTOINCREMENT,
version_id INTEGER NOT NULL,
is_applied INTEGER NOT NULL,
tstamp DATETIME
)`)
if err != nil {
t.Fatalf("create goose_db_version error = %v", err)
}
_, err = db.Exec(`INSERT INTO goose_db_version (version_id, is_applied) VALUES (0, 1), (?, 1)`, version)
if err != nil {
t.Fatalf("insert goose_db_version error = %v", err)
}
}
func callLegacy(t *testing.T, gdb *gorm.DB) {
t.Helper()
ctx := core.NewContext(context.Background())
core.Provide[contracts.DBService](ctx, testDB{db: gdb})
if err := Legacy(ctx); err != nil {
t.Fatalf("Legacy() error = %v", err)
}
}
func listStamps(t *testing.T, db *sql.DB) []stampRow {
t.Helper()
rows, err := db.Query(`SELECT plugin_id, version_id FROM w_schema_versions ORDER BY plugin_id, version_id`)
if err != nil {
t.Fatalf("list w_schema_versions error = %v", err)
}
defer func() { _ = rows.Close() }()
var got []stampRow
for rows.Next() {
var r stampRow
if err := rows.Scan(&r.PluginID, &r.VersionID); err != nil {
t.Fatalf("scan w_schema_versions error = %v", err)
}
got = append(got, r)
}
if err := rows.Err(); err != nil {
t.Fatalf("rows.Err() = %v", err)
}
return got
}
type stampRow struct {
PluginID string
VersionID int64
}
func TestLegacyStampsGoldVersionIdempotent(t *testing.T) {
gdb, sqlDB := openStampDB(t)
createSchemaVersions(t, sqlDB)
createGooseDBVersion(t, sqlDB, goldGooseVersion)
callLegacy(t, gdb)
got := listStamps(t, sqlDB)
want := []stampRow{
{PluginID: "openflare/legacy", VersionID: 0},
{PluginID: "openflare/legacy", VersionID: goldGooseVersion},
{PluginID: "server", VersionID: 1},
}
if len(got) != len(want) {
t.Fatalf("Legacy() stamps = %#v, want %#v", got, want)
}
for i := range want {
if got[i] != want[i] {
t.Errorf("Legacy() stamps[%d] = %+v, want %+v", i, got[i], want[i])
}
}
callLegacy(t, gdb)
gotAgain := listStamps(t, sqlDB)
if len(gotAgain) != len(want) {
t.Fatalf("Legacy() second call rows = %d, want %d", len(gotAgain), len(want))
}
}
func TestLegacyWithoutGooseTableDoesNotStampLegacy(t *testing.T) {
gdb, sqlDB := openStampDB(t)
createSchemaVersions(t, sqlDB)
callLegacy(t, gdb)
got := listStamps(t, sqlDB)
for _, row := range got {
if row.PluginID == "openflare/legacy" {
t.Errorf("Legacy() without goose_db_version wrote openflare/legacy %+v, want none", row)
}
}
}