mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-29 05:56:38 +08:00
feat(server): stamp legacy goose versions and own of_* migrations only
This commit is contained in:
+19
-22
@@ -2,7 +2,8 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package migrator
|
||||
// Package chmigrate applies ClickHouse of_node_* goose migrations.
|
||||
package chmigrate
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -25,15 +26,13 @@ const (
|
||||
clickhouseReadTimeoutFactor = 2
|
||||
)
|
||||
|
||||
// clickhouseMigrationFS contains SQL migrations under goose/clickhouse.
|
||||
//
|
||||
//go:embed goose/clickhouse/*.sql
|
||||
var clickhouseMigrationFS embed.FS
|
||||
|
||||
// MigrateClickHouse runs goose migrations against ClickHouse when enabled.
|
||||
func MigrateClickHouse() Report {
|
||||
// Up runs of_node_* goose migrations against ClickHouse when enabled.
|
||||
func Up() error {
|
||||
if !runtimeconfig.ClickHouseEnabled() {
|
||||
return Report{Backend: "ClickHouse"}
|
||||
return nil
|
||||
}
|
||||
|
||||
cfg := runtimeconfig.Get().ClickHouse
|
||||
@@ -58,10 +57,11 @@ func MigrateClickHouse() Report {
|
||||
BlockBufferSize: cfg.BlockBufferSize,
|
||||
})
|
||||
|
||||
subFS, err := fs.Sub(clickhouseMigrationFS, "goose/clickhouse")
|
||||
subFS, err := fs.Sub(clickhouseMigrationFS, clickhouseMigrationDir)
|
||||
if err != nil {
|
||||
closeClickHouseDB(sqlDB)
|
||||
log.Fatalf("[ClickHouse] get sub fs failed: %v\n", err)
|
||||
log.Printf("[ClickHouse] get sub fs failed: %v\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
provider, err := goose.NewProvider(
|
||||
@@ -73,32 +73,29 @@ func MigrateClickHouse() Report {
|
||||
)
|
||||
if err != nil {
|
||||
closeClickHouseDB(sqlDB)
|
||||
log.Fatalf("[ClickHouse] create goose provider failed: %v\n", err)
|
||||
log.Printf("[ClickHouse] create goose provider failed: %v\n", err)
|
||||
return err
|
||||
}
|
||||
previousVersion, err := provider.GetDBVersion(context.Background())
|
||||
if err != nil {
|
||||
if _, err := provider.GetDBVersion(context.Background()); err != nil {
|
||||
closeClickHouseDB(sqlDB)
|
||||
log.Fatalf("[ClickHouse] get goose version failed: %v\n", err)
|
||||
log.Printf("[ClickHouse] get goose version failed: %v\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := provider.Up(context.Background()); err != nil {
|
||||
closeClickHouseDB(sqlDB)
|
||||
log.Fatalf("[ClickHouse] goose migrate failed: %v\n", err)
|
||||
log.Printf("[ClickHouse] goose migrate failed: %v\n", err)
|
||||
return err
|
||||
}
|
||||
currentVersion, err := provider.GetDBVersion(context.Background())
|
||||
if err != nil {
|
||||
if _, err := provider.GetDBVersion(context.Background()); err != nil {
|
||||
closeClickHouseDB(sqlDB)
|
||||
log.Fatalf("[ClickHouse] get migrated goose version failed: %v\n", err)
|
||||
log.Printf("[ClickHouse] get migrated goose version failed: %v\n", err)
|
||||
return err
|
||||
}
|
||||
closeClickHouseDB(sqlDB)
|
||||
|
||||
log.Println("[ClickHouse] goose migrate success")
|
||||
return Report{
|
||||
Backend: "ClickHouse",
|
||||
Enabled: true,
|
||||
Version: currentVersion,
|
||||
Applied: currentVersion != previousVersion,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func closeClickHouseDB(sqlDB *sql.DB) {
|
||||
+14
-13
@@ -1,9 +1,10 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package migrator
|
||||
package chmigrate
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"Wavelet/OpenFlare/plugins/server/runtimeconfig"
|
||||
@@ -20,22 +21,20 @@ func TestClickHouseMigrationFilesEmbedded(t *testing.T) {
|
||||
t.Fatal("expected embedded ClickHouse migrations, got none")
|
||||
}
|
||||
|
||||
expected := map[string]bool{
|
||||
"202606190001_create_user_access_logs.sql": false,
|
||||
"202606200001_create_node_access_logs.sql": false,
|
||||
}
|
||||
foundNodeLogs := false
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
if _, ok := expected[entry.Name()]; ok {
|
||||
expected[entry.Name()] = true
|
||||
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
|
||||
}
|
||||
}
|
||||
for name, found := range expected {
|
||||
if !found {
|
||||
t.Fatalf("expected %s in embedded migrations", name)
|
||||
}
|
||||
if !foundNodeLogs {
|
||||
t.Fatal("expected 202606200001_create_node_access_logs.sql in embedded migrations")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +44,9 @@ func TestClickHouseGooseDialect(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateClickHouseSkipsWhenDisabled(t *testing.T) {
|
||||
func TestUpSkipsWhenDisabled(t *testing.T) {
|
||||
t.Cleanup(runtimeconfig.Override(runtimeconfig.DatabaseEnabled(), false))
|
||||
MigrateClickHouse()
|
||||
if err := Up(); err != nil {
|
||||
t.Fatalf("Up() error = %v, want nil when ClickHouse disabled", err)
|
||||
}
|
||||
}
|
||||
-2
@@ -1,7 +1,5 @@
|
||||
-- +goose Up
|
||||
-- Add TTL policies to analytics tables so ClickHouse can expire rows automatically.
|
||||
ALTER TABLE w_user_access_logs MODIFY TTL created_at + INTERVAL 180 DAY;
|
||||
|
||||
-- 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;
|
||||
|
||||
@@ -0,0 +1,499 @@
|
||||
-- +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);
|
||||
|
||||
-- +goose Down
|
||||
SELECT 1;
|
||||
@@ -0,0 +1,485 @@
|
||||
-- +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);
|
||||
|
||||
-- +goose Down
|
||||
SELECT 1;
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
-- +goose Up
|
||||
CREATE TABLE IF NOT EXISTS w_user_access_logs
|
||||
(
|
||||
id UInt64,
|
||||
user_id UInt64,
|
||||
path String,
|
||||
method String,
|
||||
ip String,
|
||||
user_agent String,
|
||||
headers String,
|
||||
status Int32,
|
||||
latency Int64,
|
||||
created_at DateTime
|
||||
)
|
||||
ENGINE = MergeTree()
|
||||
PARTITION BY toYYYYMM(created_at)
|
||||
ORDER BY (created_at, ip, user_id)
|
||||
SETTINGS index_granularity = 8192;
|
||||
|
||||
-- +goose Down
|
||||
DROP TABLE IF EXISTS w_user_access_logs;
|
||||
@@ -1,140 +0,0 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"Wavelet/OpenFlare/plugins/server/openflare/zone"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/pressly/goose/v3"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// 记账表随迁移路径不同而不同(历史链用 goose_db_version,插件化后用 w_schema_versions),
|
||||
// 比对 schema 时必须排除,否则 A/B 两路必然假性不一致。
|
||||
var bookkeepingTables = []string{"goose_db_version", "w_schema_versions"}
|
||||
|
||||
// TestDumpLegacySchema 把当前 embed 内的完整历史链应用到临时 sqlite 库并导出 schema,
|
||||
// 作为 Cordis 改造前后 schema 一致性的基线(A 路)与新架构产出(B 路)的唯一事实来源。
|
||||
//
|
||||
// OF_DUMP_SCHEMA=<path> 导出表/索引定义;OF_DUMP_VERSIONS=<path> 导出已应用版本序列。
|
||||
func TestDumpLegacySchema(t *testing.T) {
|
||||
dumpPath := os.Getenv("OF_DUMP_SCHEMA")
|
||||
if dumpPath == "" && os.Getenv("OF_DUMP_VERSIONS") == "" {
|
||||
t.Skip("OF_DUMP_SCHEMA / OF_DUMP_VERSIONS not set")
|
||||
}
|
||||
|
||||
conn, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
sqlDB, err := conn.DB()
|
||||
require.NoError(t, err)
|
||||
// :memory: 连接池仅一条连接可见,否则 goose 与 dump 会看到不同的空库。
|
||||
sqlDB.SetMaxOpenConns(1)
|
||||
|
||||
goose.SetBaseFS(migrationFS)
|
||||
require.NoError(t, goose.SetDialect(dialectSqlite))
|
||||
|
||||
// 与 Migrate() 保持同一顺序:SQL 到 zone 导入标记 → Go 侧导入 → 其余 SQL。
|
||||
require.NoError(t, goose.UpTo(sqlDB, "goose/sqlite", zoneImportSQLVersion))
|
||||
require.NoError(t, runZoneImport(sqlDB))
|
||||
require.NoError(t, goose.Up(sqlDB, "goose/sqlite"))
|
||||
|
||||
if dumpPath != "" {
|
||||
schema := dumpSchema(t, sqlDB)
|
||||
require.NoError(t, os.WriteFile(dumpPath, []byte(schema), 0o600))
|
||||
}
|
||||
if vPath := os.Getenv("OF_DUMP_VERSIONS"); vPath != "" {
|
||||
versions := dumpVersions(t, sqlDB)
|
||||
require.NoError(t, os.WriteFile(vPath, []byte(versions), 0o600))
|
||||
}
|
||||
}
|
||||
|
||||
// runZoneImport 在空库上执行 Go 侧 zone 导入,验证升级窗口钩子本身可运行(空库应为零导入)。
|
||||
func runZoneImport(sqlDB *sql.DB) error {
|
||||
ctx := context.Background()
|
||||
tx, err := sqlDB.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("begin zone import: %w", err)
|
||||
}
|
||||
report, err := zone.ImportLegacyTx(ctx, tx, false)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return report.LogAndReturn(err)
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf("commit zone import: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func dumpSchema(t *testing.T, sqlDB *sql.DB) string {
|
||||
t.Helper()
|
||||
query := `SELECT type, name, COALESCE(sql, '') FROM sqlite_master
|
||||
WHERE type IN ('table', 'index') AND name NOT LIKE 'sqlite_%'
|
||||
ORDER BY type, name`
|
||||
rows, err := sqlDB.Query(query)
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
var b strings.Builder
|
||||
for rows.Next() {
|
||||
var objType, name, ddl string
|
||||
require.NoError(t, rows.Scan(&objType, &name, &ddl))
|
||||
if isBookkeeping(name) {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(&b, "-- %s %s\n%s;\n", objType, name, strings.TrimSpace(ddl))
|
||||
}
|
||||
require.NoError(t, rows.Err())
|
||||
return b.String() + fmt.Sprintf("-- total objects (excl. bookkeeping): %d\n", countObjects(t, sqlDB))
|
||||
}
|
||||
|
||||
func countObjects(t *testing.T, sqlDB *sql.DB) int {
|
||||
t.Helper()
|
||||
query := `SELECT COUNT(*) FROM sqlite_master WHERE type IN ('table','index')
|
||||
AND name NOT LIKE 'sqlite_%'`
|
||||
var n int
|
||||
require.NoError(t, sqlDB.QueryRow(query).Scan(&n))
|
||||
for _, tbl := range bookkeepingTables {
|
||||
var has int
|
||||
require.NoError(t, sqlDB.QueryRow(
|
||||
"SELECT COUNT(*) FROM sqlite_master WHERE name = ?", tbl).Scan(&has))
|
||||
n -= has
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func dumpVersions(t *testing.T, sqlDB *sql.DB) string {
|
||||
t.Helper()
|
||||
rows, err := sqlDB.Query(
|
||||
"SELECT version_id FROM goose_db_version WHERE is_applied = 1 ORDER BY version_id")
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
var b strings.Builder
|
||||
for rows.Next() {
|
||||
var v int64
|
||||
require.NoError(t, rows.Scan(&v))
|
||||
fmt.Fprintf(&b, "%d\n", v)
|
||||
}
|
||||
require.NoError(t, rows.Err())
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func isBookkeeping(name string) bool {
|
||||
for _, tbl := range bookkeepingTables {
|
||||
if name == tbl {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
// Copyright 2025 linux.do
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package migrator 提供数据库迁移功能
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"embed"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"Wavelet/OpenFlare/plugins/server/runtimeconfig"
|
||||
db "Wavelet/plugins/infra/database"
|
||||
"Wavelet/OpenFlare/plugins/server/openflare/zone"
|
||||
"Wavelet/OpenFlare/plugins/server/repository"
|
||||
|
||||
"github.com/pressly/goose/v3"
|
||||
)
|
||||
|
||||
// migrationFS contains SQL migrations under goose/<dialect>.
|
||||
//
|
||||
//go:embed goose/postgres/*.sql goose/sqlite/*.sql
|
||||
var migrationFS embed.FS
|
||||
|
||||
// dbType 返回当前数据库类型名称(用于日志输出)
|
||||
func dbType() string {
|
||||
if !runtimeconfig.DatabaseEnabled() {
|
||||
return "SQLite"
|
||||
}
|
||||
return "PostgreSQL"
|
||||
}
|
||||
|
||||
const (
|
||||
dialectSqlite = "sqlite3"
|
||||
dialectPostgres = "postgres"
|
||||
// zoneImportSQLVersion is the goose SQL marker after of_zones creation and
|
||||
// before drop of legacy route domain columns. Zone data import runs only
|
||||
// while DB version is in [zoneImportSQLVersion, zoneDropLegacySQLVersion).
|
||||
zoneImportSQLVersion int64 = 202607120002
|
||||
zoneDropLegacySQLVersion int64 = 202607130001
|
||||
)
|
||||
|
||||
// Report describes the database migration state observed during startup.
|
||||
type Report struct {
|
||||
Backend string
|
||||
Enabled bool
|
||||
Version int64
|
||||
Applied bool
|
||||
}
|
||||
|
||||
func gooseDialect() string {
|
||||
if !runtimeconfig.DatabaseEnabled() {
|
||||
return dialectSqlite
|
||||
}
|
||||
return dialectPostgres
|
||||
}
|
||||
|
||||
func migrationDir() string {
|
||||
if !runtimeconfig.DatabaseEnabled() {
|
||||
return "goose/sqlite"
|
||||
}
|
||||
return "goose/postgres"
|
||||
}
|
||||
|
||||
// Migrate 执行数据库迁移:全部结构变更走 goose SQL;Zone 历史域名导入在 SQL 之后自动执行。
|
||||
func Migrate() Report {
|
||||
gormDB := db.DB(context.Background())
|
||||
if gormDB == nil {
|
||||
log.Fatalf("[%s] database not initialized\n", dbType())
|
||||
}
|
||||
|
||||
sqlDB, err := gormDB.DB()
|
||||
if err != nil {
|
||||
log.Fatalf("[%s] load sql db failed: %v\n", dbType(), err)
|
||||
}
|
||||
|
||||
goose.SetBaseFS(migrationFS)
|
||||
if err := goose.SetDialect(gooseDialect()); err != nil {
|
||||
log.Fatalf("[%s] set goose dialect failed: %v\n", dbType(), err)
|
||||
}
|
||||
if err := resyncGooseVersionSequence(sqlDB); err != nil {
|
||||
log.Fatalf("[%s] resync goose_db_version sequence failed: %v\n", dbType(), err)
|
||||
}
|
||||
previousVersion, err := goose.GetDBVersion(sqlDB)
|
||||
if err != nil {
|
||||
log.Fatalf("[%s] get goose version failed: %v\n", dbType(), err)
|
||||
}
|
||||
// 1) SQL up to zone-import marker (includes of_zones DDL; still has legacy columns).
|
||||
if err := goose.UpTo(sqlDB, migrationDir(), zoneImportSQLVersion); err != nil {
|
||||
log.Fatalf("[%s] goose migrate (up to zone import) failed: %v\n", dbType(), err)
|
||||
}
|
||||
// 2) Import legacy domains only during the one-time upgrade window:
|
||||
// version in [202607120002, 202607130001). After phase-2 drop is applied,
|
||||
// this is skipped on every subsequent startup.
|
||||
if err := maybeImportZoneDomains(sqlDB); err != nil {
|
||||
log.Fatalf("[%s] zone domain import failed: %v\n", dbType(), err)
|
||||
}
|
||||
// 3) Remaining SQL (drop legacy columns / managed_domains, later migrations).
|
||||
if err := goose.Up(sqlDB, migrationDir()); err != nil {
|
||||
log.Fatalf("[%s] goose migrate failed: %v\n", dbType(), err)
|
||||
}
|
||||
|
||||
clearSystemConfigCache()
|
||||
currentVersion, err := goose.GetDBVersion(sqlDB)
|
||||
if err != nil {
|
||||
log.Fatalf("[%s] get migrated goose version failed: %v\n", dbType(), err)
|
||||
}
|
||||
|
||||
log.Printf("[%s] goose migrate success\n", dbType())
|
||||
return Report{
|
||||
Backend: dbType(),
|
||||
Enabled: true,
|
||||
Version: currentVersion,
|
||||
Applied: currentVersion != previousVersion,
|
||||
}
|
||||
}
|
||||
|
||||
// maybeImportZoneDomains runs legacy→Zone import only when the DB is still in the
|
||||
// pre-drop upgrade window. After 202607130001 is applied, this is a no-op and is
|
||||
// not executed on normal restarts.
|
||||
func maybeImportZoneDomains(sqlDB *sql.DB) error {
|
||||
version, err := goose.GetDBVersion(sqlDB)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get goose db version: %w", err)
|
||||
}
|
||||
if version < zoneImportSQLVersion || version >= zoneDropLegacySQLVersion {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
tx, err := sqlDB.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("begin zone import transaction: %w", err)
|
||||
}
|
||||
report, err := zone.ImportLegacyTx(ctx, tx, gooseDialect() == dialectPostgres)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return report.LogAndReturn(err)
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf("commit zone import: %w", err)
|
||||
}
|
||||
if report.Zones > 0 || report.Domains > 0 {
|
||||
log.Printf(
|
||||
"[%s] imported zone domains automatically: zones=%d domains=%d\n",
|
||||
dbType(), report.Zones, report.Domains,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// resyncGooseVersionSequence 修复 PostgreSQL 下 goose_db_version.id 自增序列落后于
|
||||
// MAX(id) 的问题(常见于从 dump 恢复或历史迁移以显式 id 复制数据后)。序列落后会
|
||||
// 导致 goose 记录新版本号时 INSERT 命中 goose_db_version_pkey 唯一约束冲突。
|
||||
// 仅在表已存在且为 PostgreSQL 方言时执行;SQLite 使用 AUTOINCREMENT 不受影响。
|
||||
func resyncGooseVersionSequence(sqlDB *sql.DB) error {
|
||||
if gooseDialect() != dialectPostgres {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
var exists bool
|
||||
if err := sqlDB.QueryRowContext(ctx,
|
||||
"SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema='public' AND table_name='goose_db_version')",
|
||||
).Scan(&exists); err != nil {
|
||||
return fmt.Errorf("check goose_db_version existence failed: %w", err)
|
||||
}
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
|
||||
const resyncSQL = `SELECT setval(
|
||||
pg_get_serial_sequence('goose_db_version', 'id'),
|
||||
GREATEST(COALESCE((SELECT MAX(id) FROM goose_db_version), 1), 1),
|
||||
(SELECT MAX(id) IS NOT NULL FROM goose_db_version)
|
||||
)`
|
||||
if _, err := sqlDB.ExecContext(ctx, resyncSQL); err != nil {
|
||||
return fmt.Errorf("setval goose_db_version sequence failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func clearSystemConfigCache() {
|
||||
if err := repository.InvalidateAllSystemConfigCaches(context.Background()); err != nil {
|
||||
log.Printf("[%s] clear system config cache failed: %v\n", dbType(), err)
|
||||
}
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"Wavelet/OpenFlare/plugins/server/runtimeconfig"
|
||||
db "Wavelet/plugins/infra/database"
|
||||
"Wavelet/OpenFlare/plugins/server/model"
|
||||
"Wavelet/OpenFlare/plugins/server/repository"
|
||||
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/redis/go-redis/v9/maintnotifications"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// expectedMigratedSystemConfigCount 为全新库执行全部迁移后 w_system_configs 的行数
|
||||
// (初始系统配置 + 各期配置迁移/新增 seed:of_options 迁移、文件白名单、磁盘缓存、
|
||||
// 登录会话 TTL、升级源、存储、FRPS Web UI、Pages、OpenResty 限流、单 IP 限频、
|
||||
// 错误页、SW 离线、日志保留期、指标保留期等);新增配置 seed 迁移时需同步更新本常量。
|
||||
const expectedMigratedSystemConfigCount = 95
|
||||
|
||||
func TestMigrateInitializesSQLiteDatabase(t *testing.T) {
|
||||
sqliteDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
|
||||
DisableForeignKeyConstraintWhenMigrating: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("gorm.Open(sqlite) error = %v", err)
|
||||
}
|
||||
|
||||
mr, err := miniredis.Run()
|
||||
if err != nil {
|
||||
t.Fatalf("miniredis.Run() error = %v", err)
|
||||
}
|
||||
redisClient := redis.NewClient(&redis.Options{
|
||||
Addr: mr.Addr(),
|
||||
MaintNotificationsConfig: &maintnotifications.Config{
|
||||
Mode: maintnotifications.ModeDisabled,
|
||||
},
|
||||
})
|
||||
|
||||
db.SetDB(sqliteDB)
|
||||
t.Cleanup(func() {
|
||||
db.SetDB(nil)
|
||||
_ = redisClient.Close()
|
||||
mr.Close()
|
||||
})
|
||||
t.Cleanup(runtimeconfig.Override(false, runtimeconfig.ClickHouseEnabled()))
|
||||
|
||||
Migrate()
|
||||
|
||||
var systemConfigCount int64
|
||||
if err := sqliteDB.Table("w_system_configs").Count(&systemConfigCount).Error; err != nil {
|
||||
t.Fatalf("Migrate() count w_system_configs error = %v", err)
|
||||
}
|
||||
if systemConfigCount != expectedMigratedSystemConfigCount {
|
||||
t.Errorf("Migrate() w_system_configs count = %d, want %d", systemConfigCount, expectedMigratedSystemConfigCount)
|
||||
}
|
||||
|
||||
var adminCount int64
|
||||
if err := sqliteDB.Table("w_users").Where("username = ?", "admin").Count(&adminCount).Error; err != nil {
|
||||
t.Fatalf("Migrate() count admin user error = %v", err)
|
||||
}
|
||||
if adminCount != 1 {
|
||||
t.Errorf("Migrate() admin user count = %d, want %d", adminCount, 1)
|
||||
}
|
||||
|
||||
var templateCount int64
|
||||
if err := sqliteDB.Table("w_templates").Count(&templateCount).Error; err != nil {
|
||||
t.Fatalf("Migrate() count templates error = %v", err)
|
||||
}
|
||||
if templateCount != 2 {
|
||||
t.Errorf("Migrate() templates count = %d, want %d", templateCount, 2)
|
||||
}
|
||||
|
||||
if !sqliteDB.Migrator().HasTable("of_zones") {
|
||||
t.Error("Migrate() did not create of_zones")
|
||||
}
|
||||
if !sqliteDB.Migrator().HasTable("of_zone_domains") {
|
||||
t.Error("Migrate() did not create of_zone_domains")
|
||||
}
|
||||
for _, table := range []string{
|
||||
"of_cf_connections",
|
||||
"of_cf_pointing_groups",
|
||||
"of_cf_pointing_members",
|
||||
} {
|
||||
if !sqliteDB.Migrator().HasTable(table) {
|
||||
t.Errorf("Migrate() did not create %s", table)
|
||||
}
|
||||
}
|
||||
if !sqliteDB.Migrator().HasColumn("of_cf_connections", "authorization") {
|
||||
t.Error("Migrate() did not create of_cf_connections.authorization")
|
||||
}
|
||||
if sqliteDB.Migrator().HasTable("of_managed_domains") {
|
||||
t.Error("Migrate() should drop of_managed_domains after phase-2 cleanup")
|
||||
}
|
||||
if sqliteDB.Migrator().HasColumn(&model.ProxyRoute{}, "domain") {
|
||||
t.Error("Migrate() should drop of_proxy_routes.domain after phase-2 cleanup")
|
||||
}
|
||||
if sqliteDB.Migrator().HasColumn(&model.ProxyRoute{}, "domains") {
|
||||
t.Error("Migrate() should drop of_proxy_routes.domains after phase-2 cleanup")
|
||||
}
|
||||
if sqliteDB.Migrator().HasColumn(&model.ProxyRoute{}, "cert_id") {
|
||||
t.Error("Migrate() should drop of_proxy_routes.cert_id after phase-2 cleanup")
|
||||
}
|
||||
if sqliteDB.Migrator().HasColumn(&model.ProxyRoute{}, "cert_ids") {
|
||||
t.Error("Migrate() should drop of_proxy_routes.cert_ids after phase-2 cleanup")
|
||||
}
|
||||
if sqliteDB.Migrator().HasColumn(&model.ProxyRoute{}, "domain_cert_ids") {
|
||||
t.Error("Migrate() should drop of_proxy_routes.domain_cert_ids after phase-2 cleanup")
|
||||
}
|
||||
|
||||
zone := model.Zone{Domain: "example.com"}
|
||||
if err := sqliteDB.Create(&zone).Error; err != nil {
|
||||
t.Fatalf("Migrate() create Zone error = %v", err)
|
||||
}
|
||||
if err := sqliteDB.Create(&model.Zone{Domain: zone.Domain}).Error; err == nil {
|
||||
t.Error("Migrate() allowed duplicate of_zones.domain")
|
||||
}
|
||||
|
||||
domain := model.ZoneDomain{ZoneID: zone.ID, Domain: "api.example.com"}
|
||||
if err := sqliteDB.Create(&domain).Error; err != nil {
|
||||
t.Fatalf("Migrate() create ZoneDomain error = %v", err)
|
||||
}
|
||||
if err := sqliteDB.Create(&model.ZoneDomain{ZoneID: zone.ID, Domain: domain.Domain}).Error; err == nil {
|
||||
t.Error("Migrate() allowed duplicate of_zone_domains.domain")
|
||||
}
|
||||
|
||||
if err := sqliteDB.Exec(`INSERT INTO of_cf_pointing_members
|
||||
(group_id, zone_domain_id, proxied, cf_zone_id, cf_record_id, desired_ip, sync_status, last_error)
|
||||
VALUES (?, ?, ?, '', '', '', 'pending', '')`, 1, domain.ID, false).Error; err != nil {
|
||||
t.Fatalf("Migrate() insert Cloudflare member error = %v", err)
|
||||
}
|
||||
if err := sqliteDB.Exec(`INSERT INTO of_cf_pointing_members
|
||||
(group_id, zone_domain_id, proxied, cf_zone_id, cf_record_id, desired_ip, sync_status, last_error)
|
||||
VALUES (?, ?, ?, '', '', '', 'pending', '')`, 2, domain.ID, false).Error; err == nil {
|
||||
t.Error("Migrate() allowed duplicate of_cf_pointing_members.zone_domain_id")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloudflarePointingPostgresMigrationQuotesAuthorizationColumn(t *testing.T) {
|
||||
content, err := migrationFS.ReadFile("goose/postgres/202608040001_create_cloudflare_pointing.sql")
|
||||
if err != nil {
|
||||
t.Fatalf("read Cloudflare pointing migration: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(content), `"authorization" TEXT NOT NULL DEFAULT ''`) {
|
||||
t.Error("Cloudflare pointing PostgreSQL migration must quote reserved column authorization")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateSeedsSystemConfigs(t *testing.T) {
|
||||
sqliteDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
|
||||
DisableForeignKeyConstraintWhenMigrating: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("gorm.Open(sqlite) error = %v", err)
|
||||
}
|
||||
|
||||
db.SetDB(sqliteDB)
|
||||
t.Cleanup(func() { db.SetDB(nil) })
|
||||
t.Cleanup(runtimeconfig.Override(false, runtimeconfig.ClickHouseEnabled()))
|
||||
|
||||
Migrate()
|
||||
|
||||
enabled, err := repository.GetBoolByKey(context.Background(), model.ConfigKeyEmailLoginVerificationEnabled)
|
||||
if err != nil {
|
||||
t.Fatalf("GetBoolByKey(%s) error = %v", model.ConfigKeyEmailLoginVerificationEnabled, err)
|
||||
}
|
||||
if enabled {
|
||||
t.Fatalf("GetBoolByKey(%s) = true, want false", model.ConfigKeyEmailLoginVerificationEnabled)
|
||||
}
|
||||
}
|
||||
@@ -1,323 +0,0 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"Wavelet/OpenFlare/plugins/server/model"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/pressly/goose/v3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
pagesSourcePreviousMigration = int64(202607190001)
|
||||
pagesSourceMigration = int64(202607190002)
|
||||
pagesMigrationProjectID = uint(900001)
|
||||
pagesMigrationDeploymentID = uint(900001)
|
||||
)
|
||||
|
||||
func TestPagesSourceMigrationSQLiteUpDownUp(t *testing.T) {
|
||||
dbPath := t.TempDir() + "/pages-source-migration.db"
|
||||
gormDB, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{
|
||||
DisableForeignKeyConstraintWhenMigrating: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
sqlDB, err := gormDB.DB()
|
||||
require.NoError(t, err)
|
||||
sqlDB.SetMaxOpenConns(1)
|
||||
t.Cleanup(func() { require.NoError(t, sqlDB.Close()) })
|
||||
|
||||
runPagesSourceMigrationUpDownUp(t, gormDB, sqlDB, dialectSqlite, "goose/sqlite")
|
||||
|
||||
var indexSQL string
|
||||
require.NoError(t, gormDB.Raw(
|
||||
"SELECT sql FROM sqlite_master WHERE type = 'index' AND name = ?",
|
||||
"idx_of_pages_deployments_source_revision",
|
||||
).Scan(&indexSQL).Error)
|
||||
assert.Contains(t, strings.ToUpper(indexSQL), "WHERE SOURCE_IDENTITY IS NOT NULL AND SOURCE_REVISION IS NOT NULL")
|
||||
}
|
||||
|
||||
func TestPagesSourceMigrationPostgresUpDownUp(t *testing.T) {
|
||||
dsn := strings.TrimSpace(os.Getenv("OPENFLARE_TEST_POSTGRES_DSN"))
|
||||
if dsn == "" {
|
||||
t.Skip("OPENFLARE_TEST_POSTGRES_DSN is not set")
|
||||
}
|
||||
|
||||
gormDB, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
|
||||
DisableForeignKeyConstraintWhenMigrating: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
sqlDB, err := gormDB.DB()
|
||||
require.NoError(t, err)
|
||||
sqlDB.SetMaxOpenConns(1)
|
||||
|
||||
schema := fmt.Sprintf("pages_source_migration_%d", time.Now().UnixNano())
|
||||
require.Regexp(t, `^[a-z0-9_]+$`, schema)
|
||||
require.NoError(t, gormDB.Exec(`CREATE SCHEMA "`+schema+`"`).Error)
|
||||
require.NoError(t, gormDB.Exec(`SET search_path TO "`+schema+`"`).Error)
|
||||
t.Cleanup(func() {
|
||||
assert.NoError(t, gormDB.Exec("SET search_path TO public").Error)
|
||||
assert.NoError(t, gormDB.Exec(`DROP SCHEMA IF EXISTS "`+schema+`" CASCADE`).Error)
|
||||
assert.NoError(t, sqlDB.Close())
|
||||
})
|
||||
|
||||
runPagesSourceMigrationUpDownUp(t, gormDB, sqlDB, dialectPostgres, "goose/postgres")
|
||||
}
|
||||
|
||||
func runPagesSourceMigrationUpDownUp(
|
||||
t *testing.T,
|
||||
gormDB *gorm.DB,
|
||||
sqlDB *sql.DB,
|
||||
dialect string,
|
||||
dir string,
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
goose.SetBaseFS(migrationFS)
|
||||
require.NoError(t, goose.SetDialect(dialect))
|
||||
require.NoError(t, goose.UpTo(sqlDB, dir, pagesSourcePreviousMigration))
|
||||
seedPrePagesSourceMigrationData(t, gormDB)
|
||||
|
||||
require.NoError(t, goose.UpTo(sqlDB, dir, pagesSourceMigration))
|
||||
assertPagesSourceMigrationUp(t, gormDB)
|
||||
|
||||
require.NoError(t, goose.DownTo(sqlDB, dir, pagesSourcePreviousMigration))
|
||||
assertPagesSourceMigrationDown(t, gormDB)
|
||||
|
||||
require.NoError(t, goose.UpTo(sqlDB, dir, pagesSourceMigration))
|
||||
assertPagesSourceMigrationUpAgain(t, gormDB)
|
||||
}
|
||||
|
||||
func seedPrePagesSourceMigrationData(t *testing.T, gormDB *gorm.DB) {
|
||||
t.Helper()
|
||||
|
||||
require.NoError(t, gormDB.Exec(`
|
||||
INSERT INTO of_pages_projects (
|
||||
id, name, slug, description, enabled, active_deployment_id, root_dir, entry_file
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
pagesMigrationProjectID,
|
||||
"Migration Site",
|
||||
"migration-site",
|
||||
"keep-project-data",
|
||||
true,
|
||||
pagesMigrationDeploymentID,
|
||||
"public",
|
||||
"home.html",
|
||||
).Error)
|
||||
require.NoError(t, gormDB.Exec(`
|
||||
INSERT INTO of_pages_deployments (
|
||||
id, project_id, deployment_number, checksum, status, upload_id, artifact_path,
|
||||
file_count, total_size, created_by
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
pagesMigrationDeploymentID,
|
||||
pagesMigrationProjectID,
|
||||
1,
|
||||
strings.Repeat("a", 64),
|
||||
model.PagesDeploymentStatusActive,
|
||||
uint64(700001),
|
||||
"legacy/package.zip",
|
||||
2,
|
||||
int64(128),
|
||||
"user:1",
|
||||
).Error)
|
||||
}
|
||||
|
||||
func assertPagesSourceMigrationUp(t *testing.T, gormDB *gorm.DB) {
|
||||
t.Helper()
|
||||
migrator := gormDB.Migrator()
|
||||
assert.True(t, migrator.HasTable(&model.PagesProjectSource{}))
|
||||
assert.True(t, migrator.HasTable(&model.PagesProjectSourceRuntime{}))
|
||||
assert.True(t, migrator.HasColumn(&model.PagesProject{}, "ContentConfigVersion"))
|
||||
assert.True(t, migrator.HasColumn(&model.PagesDeployment{}, "SourceType"))
|
||||
assert.True(t, migrator.HasColumn(&model.PagesDeployment{}, "SourceIdentity"))
|
||||
assert.True(t, migrator.HasColumn(&model.PagesDeployment{}, "SourceRevision"))
|
||||
assert.True(t, migrator.HasIndex(&model.PagesProjectSource{}, "idx_of_pages_project_sources_project_id"))
|
||||
assert.True(t, migrator.HasIndex(&model.PagesProjectSourceRuntime{}, "idx_of_pages_project_source_runtime_next_check_at"))
|
||||
assert.True(t, migrator.HasIndex(&model.PagesDeployment{}, "idx_of_pages_deployments_project_number"))
|
||||
assert.True(t, migrator.HasIndex(&model.PagesDeployment{}, "idx_of_pages_deployments_source_revision"))
|
||||
|
||||
var project model.PagesProject
|
||||
require.NoError(t, gormDB.First(&project, pagesMigrationProjectID).Error)
|
||||
assert.Equal(t, 0, project.ContentConfigVersion)
|
||||
assert.Equal(t, "keep-project-data", project.Description)
|
||||
assert.Equal(t, "public", project.RootDir)
|
||||
assert.Equal(t, "home.html", project.EntryFile)
|
||||
|
||||
var deployment model.PagesDeployment
|
||||
require.NoError(t, gormDB.First(&deployment, pagesMigrationDeploymentID).Error)
|
||||
assert.Equal(t, "manual_upload", deployment.SourceType)
|
||||
assert.Equal(t, "manual_upload", deployment.TriggerType)
|
||||
assert.Nil(t, deployment.SourceIdentity)
|
||||
assert.Nil(t, deployment.SourceRevision)
|
||||
assert.Equal(t, uint64(700001), deployment.UploadID)
|
||||
|
||||
sourceID := createMigrationSourceRuntime(t, gormDB)
|
||||
assertPagesSourceConstraints(t, gormDB, sourceID)
|
||||
}
|
||||
|
||||
func createMigrationSourceRuntime(t *testing.T, gormDB *gorm.DB) uint {
|
||||
t.Helper()
|
||||
source := model.PagesProjectSource{
|
||||
ProjectID: pagesMigrationProjectID,
|
||||
SourceType: "remote_url",
|
||||
RemoteURL: "https://example.com/site.zip?token=secret",
|
||||
AllowInsecure: false,
|
||||
CheckIntervalMinutes: 0,
|
||||
ConfigVersion: 1,
|
||||
SourceIdentity: strings.Repeat("b", 64),
|
||||
}
|
||||
require.NoError(t, gormDB.Create(&source).Error)
|
||||
require.NotZero(t, source.ID)
|
||||
require.NoError(t, gormDB.Create(&model.PagesProjectSourceRuntime{
|
||||
SourceID: source.ID,
|
||||
SyncStatus: "idle",
|
||||
}).Error)
|
||||
return source.ID
|
||||
}
|
||||
|
||||
func assertPagesSourceConstraints(t *testing.T, gormDB *gorm.DB, sourceID uint) {
|
||||
t.Helper()
|
||||
|
||||
duplicateSource := model.PagesProjectSource{
|
||||
ProjectID: pagesMigrationProjectID,
|
||||
SourceType: "remote_url",
|
||||
ConfigVersion: 1,
|
||||
SourceIdentity: strings.Repeat("c", 64),
|
||||
}
|
||||
require.Error(t, gormDB.Create(&duplicateSource).Error)
|
||||
|
||||
for number := 2; number <= 3; number++ {
|
||||
require.NoError(t, createMigrationDeployment(
|
||||
gormDB,
|
||||
number,
|
||||
strings.Repeat(string(rune('a'+number)), 64),
|
||||
nil,
|
||||
nil,
|
||||
))
|
||||
}
|
||||
|
||||
identity := strings.Repeat("d", 64)
|
||||
revision := strings.Repeat("e", 64)
|
||||
require.NoError(t, createMigrationDeployment(
|
||||
gormDB,
|
||||
4,
|
||||
strings.Repeat("f", 64),
|
||||
&identity,
|
||||
&revision,
|
||||
))
|
||||
require.Error(t, createMigrationDeployment(
|
||||
gormDB,
|
||||
5,
|
||||
strings.Repeat("0", 64),
|
||||
&identity,
|
||||
&revision,
|
||||
))
|
||||
require.Error(t, createMigrationDeployment(
|
||||
gormDB,
|
||||
1,
|
||||
strings.Repeat("1", 64),
|
||||
nil,
|
||||
nil,
|
||||
))
|
||||
|
||||
var runtime model.PagesProjectSourceRuntime
|
||||
require.NoError(t, gormDB.First(&runtime, sourceID).Error)
|
||||
assert.Equal(t, "idle", runtime.SyncStatus)
|
||||
}
|
||||
|
||||
func createMigrationDeployment(
|
||||
gormDB *gorm.DB,
|
||||
deploymentNumber int,
|
||||
checksum string,
|
||||
identity *string,
|
||||
revision *string,
|
||||
) error {
|
||||
return gormDB.Create(&model.PagesDeployment{
|
||||
ProjectID: pagesMigrationProjectID,
|
||||
DeploymentNumber: deploymentNumber,
|
||||
Checksum: checksum,
|
||||
Status: model.PagesDeploymentStatusUploaded,
|
||||
UploadID: uint64(710000 + deploymentNumber),
|
||||
ArtifactPath: fmt.Sprintf("legacy/%d.zip", deploymentNumber),
|
||||
SourceType: "manual_upload",
|
||||
SourceIdentity: identity,
|
||||
SourceRevision: revision,
|
||||
TriggerType: "manual_upload",
|
||||
}).Error
|
||||
}
|
||||
|
||||
func assertPagesSourceMigrationDown(t *testing.T, gormDB *gorm.DB) {
|
||||
t.Helper()
|
||||
migrator := gormDB.Migrator()
|
||||
assert.False(t, migrator.HasTable(&model.PagesProjectSource{}))
|
||||
assert.False(t, migrator.HasTable(&model.PagesProjectSourceRuntime{}))
|
||||
assert.False(t, migrator.HasColumn(&model.PagesProject{}, "ContentConfigVersion"))
|
||||
assert.False(t, migrator.HasColumn(&model.PagesDeployment{}, "SourceType"))
|
||||
assert.False(t, migrator.HasColumn(&model.PagesDeployment{}, "SourceIdentity"))
|
||||
assert.False(t, migrator.HasColumn(&model.PagesDeployment{}, "SourceRevision"))
|
||||
assert.False(t, migrator.HasIndex(&model.PagesDeployment{}, "idx_of_pages_deployments_project_number"))
|
||||
assert.False(t, migrator.HasIndex(&model.PagesDeployment{}, "idx_of_pages_deployments_source_revision"))
|
||||
assert.True(t, migrator.HasIndex(&model.PagesDeployment{}, "idx_of_pages_deployments_project_id"))
|
||||
assert.True(t, migrator.HasIndex(&model.PagesDeployment{}, "idx_of_pages_deployments_upload_id"))
|
||||
|
||||
var project struct {
|
||||
Description string
|
||||
RootDir string
|
||||
EntryFile string
|
||||
ActiveDeploymentID *uint
|
||||
}
|
||||
require.NoError(t, gormDB.Table("of_pages_projects").Where("id = ?", pagesMigrationProjectID).Take(&project).Error)
|
||||
assert.Equal(t, "keep-project-data", project.Description)
|
||||
assert.Equal(t, "public", project.RootDir)
|
||||
assert.Equal(t, "home.html", project.EntryFile)
|
||||
require.NotNil(t, project.ActiveDeploymentID)
|
||||
assert.Equal(t, pagesMigrationDeploymentID, *project.ActiveDeploymentID)
|
||||
|
||||
var deployment struct {
|
||||
UploadID uint64
|
||||
ArtifactPath string
|
||||
FileCount int
|
||||
TotalSize int64
|
||||
}
|
||||
require.NoError(t, gormDB.Table("of_pages_deployments").Where("id = ?", pagesMigrationDeploymentID).Take(&deployment).Error)
|
||||
assert.Equal(t, uint64(700001), deployment.UploadID)
|
||||
assert.Equal(t, "legacy/package.zip", deployment.ArtifactPath)
|
||||
assert.Equal(t, 2, deployment.FileCount)
|
||||
assert.Equal(t, int64(128), deployment.TotalSize)
|
||||
|
||||
var count int64
|
||||
require.NoError(t, gormDB.Table("of_pages_deployments").Where("project_id = ?", pagesMigrationProjectID).Count(&count).Error)
|
||||
assert.Equal(t, int64(4), count)
|
||||
}
|
||||
|
||||
func assertPagesSourceMigrationUpAgain(t *testing.T, gormDB *gorm.DB) {
|
||||
t.Helper()
|
||||
assert.True(t, gormDB.Migrator().HasTable(&model.PagesProjectSource{}))
|
||||
assert.True(t, gormDB.Migrator().HasTable(&model.PagesProjectSourceRuntime{}))
|
||||
assert.True(t, gormDB.Migrator().HasColumn(&model.PagesProject{}, "ContentConfigVersion"))
|
||||
assert.True(t, gormDB.Migrator().HasColumn(&model.PagesDeployment{}, "SourceRevision"))
|
||||
|
||||
var count int64
|
||||
require.NoError(t, gormDB.Table("of_pages_deployments").
|
||||
Where("project_id = ? AND source_type = ? AND trigger_type = ?", pagesMigrationProjectID, "manual_upload", "manual_upload").
|
||||
Count(&count).Error)
|
||||
assert.Equal(t, int64(4), count)
|
||||
|
||||
require.NoError(t, gormDB.Table("of_pages_project_sources").Count(&count).Error)
|
||||
assert.Zero(t, count, "source config is intentionally removed by Down and is not reconstructable")
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"Wavelet/OpenFlare/plugins/server/model"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/pressly/goose/v3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
pagesSourceScanPreviousMigration = int64(202607190002)
|
||||
pagesSourceScanMigration = int64(202607190003)
|
||||
pagesSourceScanTaskType = "of_pages_source_scan"
|
||||
)
|
||||
|
||||
func TestPagesSourceScanScheduleMigrationSQLite(t *testing.T) {
|
||||
dbPath := t.TempDir() + "/pages-source-scan-migration.db"
|
||||
gormDB, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{
|
||||
DisableForeignKeyConstraintWhenMigrating: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
sqlDB, err := gormDB.DB()
|
||||
require.NoError(t, err)
|
||||
sqlDB.SetMaxOpenConns(1)
|
||||
t.Cleanup(func() { require.NoError(t, sqlDB.Close()) })
|
||||
|
||||
runPagesSourceScanScheduleMigration(t, gormDB, sqlDB, dialectSqlite, "goose/sqlite")
|
||||
}
|
||||
|
||||
func TestPagesSourceScanScheduleMigrationPostgres(t *testing.T) {
|
||||
dsn := strings.TrimSpace(os.Getenv("OPENFLARE_TEST_POSTGRES_DSN"))
|
||||
if dsn == "" {
|
||||
t.Skip("OPENFLARE_TEST_POSTGRES_DSN is not set")
|
||||
}
|
||||
gormDB, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
|
||||
DisableForeignKeyConstraintWhenMigrating: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
sqlDB, err := gormDB.DB()
|
||||
require.NoError(t, err)
|
||||
sqlDB.SetMaxOpenConns(1)
|
||||
|
||||
schema := fmt.Sprintf("pages_source_scan_migration_%d", time.Now().UnixNano())
|
||||
require.Regexp(t, `^[a-z0-9_]+$`, schema)
|
||||
require.NoError(t, gormDB.Exec(`CREATE SCHEMA "`+schema+`"`).Error)
|
||||
require.NoError(t, gormDB.Exec(`SET search_path TO "`+schema+`"`).Error)
|
||||
t.Cleanup(func() {
|
||||
assert.NoError(t, gormDB.Exec("SET search_path TO public").Error)
|
||||
assert.NoError(t, gormDB.Exec(`DROP SCHEMA IF EXISTS "`+schema+`" CASCADE`).Error)
|
||||
assert.NoError(t, sqlDB.Close())
|
||||
})
|
||||
|
||||
runPagesSourceScanScheduleMigration(t, gormDB, sqlDB, dialectPostgres, "goose/postgres")
|
||||
}
|
||||
|
||||
func runPagesSourceScanScheduleMigration(
|
||||
t *testing.T,
|
||||
gormDB *gorm.DB,
|
||||
sqlDB *sql.DB,
|
||||
dialect string,
|
||||
dir string,
|
||||
) {
|
||||
t.Helper()
|
||||
goose.SetBaseFS(migrationFS)
|
||||
require.NoError(t, goose.SetDialect(dialect))
|
||||
require.NoError(t, goose.UpTo(sqlDB, dir, pagesSourceScanPreviousMigration))
|
||||
|
||||
var previousMaxID uint64
|
||||
require.NoError(t, gormDB.Table("w_schedules").Select("COALESCE(MAX(id), 0)").Scan(&previousMaxID).Error)
|
||||
require.NoError(t, goose.UpTo(sqlDB, dir, pagesSourceScanMigration))
|
||||
seeded := assertPagesSourceScanSchedule(t, gormDB)
|
||||
assert.NotZero(t, seeded.ID)
|
||||
if dialect == dialectPostgres {
|
||||
assert.Greater(t, seeded.ID, previousMaxID)
|
||||
}
|
||||
|
||||
require.NoError(t, goose.DownTo(sqlDB, dir, pagesSourceScanPreviousMigration))
|
||||
assertPagesSourceScanScheduleMissing(t, gormDB)
|
||||
|
||||
custom := model.Schedule{
|
||||
ID: 900001,
|
||||
Name: "用户保留的 Pages 扫描任务",
|
||||
TaskType: pagesSourceScanTaskType,
|
||||
Cron: "0 * * * *",
|
||||
Payload: `{"custom":true}`,
|
||||
IsActive: false,
|
||||
}
|
||||
require.NoError(t, gormDB.Create(&custom).Error)
|
||||
require.NoError(t, goose.UpTo(sqlDB, dir, pagesSourceScanMigration))
|
||||
var schedules []model.Schedule
|
||||
require.NoError(t, gormDB.Where("task_type = ?", pagesSourceScanTaskType).Find(&schedules).Error)
|
||||
require.Len(t, schedules, 1)
|
||||
assert.Equal(t, custom.ID, schedules[0].ID)
|
||||
assert.Equal(t, custom.Name, schedules[0].Name)
|
||||
|
||||
require.NoError(t, goose.DownTo(sqlDB, dir, pagesSourceScanPreviousMigration))
|
||||
var retained model.Schedule
|
||||
require.NoError(t, gormDB.First(&retained, custom.ID).Error)
|
||||
assert.Equal(t, custom.TaskType, retained.TaskType)
|
||||
}
|
||||
|
||||
func TestPagesSourceScanScheduleMigrationsUseDatabaseGeneratedIDs(t *testing.T) {
|
||||
for _, name := range []string{
|
||||
"goose/postgres/202607190003_seed_pages_source_scan.sql",
|
||||
"goose/sqlite/202607190003_seed_pages_source_scan.sql",
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
content, err := migrationFS.ReadFile(name)
|
||||
require.NoError(t, err)
|
||||
normalized := strings.ToLower(string(content))
|
||||
assert.NotContains(t, normalized, "insert into w_schedules (id,")
|
||||
assert.NotContains(t, normalized, "coalesce(max(id)")
|
||||
})
|
||||
}
|
||||
|
||||
postgresContent, err := migrationFS.ReadFile("goose/postgres/202607190003_seed_pages_source_scan.sql")
|
||||
require.NoError(t, err)
|
||||
compactPostgres := strings.Join(strings.Fields(strings.ToLower(string(postgresContent))), " ")
|
||||
assert.Contains(
|
||||
t,
|
||||
compactPostgres,
|
||||
"select setval( pg_get_serial_sequence('w_schedules', 'id'), greatest( 1,",
|
||||
"sequence synchronization must retain a valid lower bound for an empty table",
|
||||
)
|
||||
}
|
||||
|
||||
func assertPagesSourceScanSchedule(t *testing.T, gormDB *gorm.DB) model.Schedule {
|
||||
t.Helper()
|
||||
var schedules []model.Schedule
|
||||
require.NoError(t, gormDB.Where("task_type = ?", pagesSourceScanTaskType).Find(&schedules).Error)
|
||||
require.Len(t, schedules, 1)
|
||||
schedule := schedules[0]
|
||||
assert.Equal(t, "OpenFlare Pages 部署源扫描", schedule.Name)
|
||||
assert.Equal(t, "0 0 * * *", schedule.Cron)
|
||||
assert.Equal(t, "{}", schedule.Payload)
|
||||
assert.True(t, schedule.IsActive)
|
||||
return schedule
|
||||
}
|
||||
|
||||
func assertPagesSourceScanScheduleMissing(t *testing.T, gormDB *gorm.DB) {
|
||||
t.Helper()
|
||||
var count int64
|
||||
require.NoError(t, gormDB.Model(&model.Schedule{}).
|
||||
Where("task_type = ?", pagesSourceScanTaskType).
|
||||
Count(&count).Error)
|
||||
assert.Zero(t, count)
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"Wavelet/OpenFlare/plugins/server/model"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/pressly/goose/v3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
systemCleanupPreviousMigration = int64(202608090001)
|
||||
systemCleanupMigration = int64(202608090002)
|
||||
systemCleanupTaskType = "system_cleanup"
|
||||
)
|
||||
|
||||
func TestSystemCleanupScheduleMigrationSQLite(t *testing.T) {
|
||||
dbPath := t.TempDir() + "/system-cleanup-migration.db"
|
||||
gormDB, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{
|
||||
DisableForeignKeyConstraintWhenMigrating: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
sqlDB, err := gormDB.DB()
|
||||
require.NoError(t, err)
|
||||
sqlDB.SetMaxOpenConns(1)
|
||||
t.Cleanup(func() { require.NoError(t, sqlDB.Close()) })
|
||||
|
||||
runSystemCleanupScheduleMigration(t, gormDB, sqlDB, dialectSqlite, "goose/sqlite")
|
||||
}
|
||||
|
||||
func TestSystemCleanupScheduleMigrationPostgres(t *testing.T) {
|
||||
dsn := strings.TrimSpace(os.Getenv("OPENFLARE_TEST_POSTGRES_DSN"))
|
||||
if dsn == "" {
|
||||
t.Skip("OPENFLARE_TEST_POSTGRES_DSN is not set")
|
||||
}
|
||||
gormDB, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
|
||||
DisableForeignKeyConstraintWhenMigrating: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
sqlDB, err := gormDB.DB()
|
||||
require.NoError(t, err)
|
||||
sqlDB.SetMaxOpenConns(1)
|
||||
|
||||
schema := fmt.Sprintf("system_cleanup_migration_%d", time.Now().UnixNano())
|
||||
require.Regexp(t, `^[a-z0-9_]+$`, schema)
|
||||
require.NoError(t, gormDB.Exec(`CREATE SCHEMA "`+schema+`"`).Error)
|
||||
require.NoError(t, gormDB.Exec(`SET search_path TO "`+schema+`"`).Error)
|
||||
t.Cleanup(func() {
|
||||
assert.NoError(t, gormDB.Exec("SET search_path TO public").Error)
|
||||
assert.NoError(t, gormDB.Exec(`DROP SCHEMA IF EXISTS "`+schema+`" CASCADE`).Error)
|
||||
assert.NoError(t, sqlDB.Close())
|
||||
})
|
||||
|
||||
runSystemCleanupScheduleMigration(t, gormDB, sqlDB, dialectPostgres, "goose/postgres")
|
||||
}
|
||||
|
||||
func runSystemCleanupScheduleMigration(
|
||||
t *testing.T,
|
||||
gormDB *gorm.DB,
|
||||
sqlDB *sql.DB,
|
||||
dialect string,
|
||||
dir string,
|
||||
) {
|
||||
t.Helper()
|
||||
goose.SetBaseFS(migrationFS)
|
||||
require.NoError(t, goose.SetDialect(dialect))
|
||||
require.NoError(t, goose.UpTo(sqlDB, dir, systemCleanupPreviousMigration))
|
||||
|
||||
assertSystemCleanupCron(t, gormDB, "0 */2 * * *", "迁移前应为每 2 小时")
|
||||
|
||||
require.NoError(t, goose.UpTo(sqlDB, dir, systemCleanupMigration))
|
||||
assertSystemCleanupCron(t, gormDB, "0 3 * * *", "迁移后应为每日凌晨 3 点")
|
||||
|
||||
require.NoError(t, goose.DownTo(sqlDB, dir, systemCleanupPreviousMigration))
|
||||
assertSystemCleanupCron(t, gormDB, "0 */2 * * *", "回滚后恢复每 2 小时")
|
||||
}
|
||||
|
||||
func assertSystemCleanupCron(t *testing.T, gormDB *gorm.DB, wantCron, msg string) {
|
||||
t.Helper()
|
||||
var schedule model.Schedule
|
||||
require.NoError(t, gormDB.Where("task_type = ?", systemCleanupTaskType).First(&schedule).Error)
|
||||
assert.Equal(t, wantCron, schedule.Cron, msg)
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"Wavelet/OpenFlare/plugins/server/chmigrate"
|
||||
"Wavelet/OpenFlare/plugins/server/ofevents"
|
||||
"Wavelet/OpenFlare/plugins/server/openflare/chwriter"
|
||||
ofgeoip "Wavelet/OpenFlare/plugins/server/openflare/geoip"
|
||||
@@ -22,6 +23,7 @@ import (
|
||||
"Wavelet/pkg/logger"
|
||||
"Wavelet/plugins/infra/database"
|
||||
"context"
|
||||
"embed"
|
||||
"reflect"
|
||||
|
||||
"Wavelet/OpenFlare/plugins/server/openflare/credential"
|
||||
@@ -34,6 +36,9 @@ import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
//go:embed migrations/*/*.sql
|
||||
var serverMigrations embed.FS
|
||||
|
||||
// Plugin 实现 core.Plugin,是 OpenFlare 控制面的装载入口。
|
||||
type Plugin struct{}
|
||||
|
||||
@@ -66,6 +71,11 @@ func (p *Plugin) Apply(ctx *core.Context) error {
|
||||
})
|
||||
credential.SetSessionSecret(runtimeconfig.SessionSecret())
|
||||
|
||||
ctx.Migrations().Register("server", serverMigrations)
|
||||
if err := chmigrate.Up(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ts, err := core.Inject[contracts.TaskService](ctx); err == nil && ts != nil {
|
||||
oftask.SetService(ts)
|
||||
} else {
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package stamp records pre-Cordis schema versions into w_schema_versions.
|
||||
package stamp
|
||||
|
||||
import "Wavelet/core"
|
||||
|
||||
// Legacy is the pre-Cordis schema stamp hook. Task 14 fills the body; until then
|
||||
// it is a no-op so plugin migrations can proceed on a fresh database.
|
||||
func Legacy(*core.Context) error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package stamp records pre-Cordis schema versions into w_schema_versions.
|
||||
package stamp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"unicode"
|
||||
|
||||
"Wavelet/OpenFlare/plugins/server/openflare/zone"
|
||||
"Wavelet/core"
|
||||
"Wavelet/core/contracts"
|
||||
)
|
||||
|
||||
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 stamp
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ sidebar: false
|
||||
- 控制面改为与上游 Wavelet 同构装配:`newOpenFlareApp` 挂载 Wavelet 平台插件后再挂 OpenFlare `server` 业务路由,健康检查/用户/验证码由上游插件提供;`app.redirect_trailing_slash` 默认关闭,避免列表接口尾部斜杠被 301。
|
||||
- 删除 OpenFlare 内与 Wavelet 重复的 oauth/cap/user/upload/config/health/admin 平台副本,业务改走契约(登录中间件、公共配置、推送注册、异步任务);用户/文件/系统配置由上游插件提供,控制台接口形状保持金标准子集。
|
||||
- 控制面读写系统配置改为走上游管理仓储并同步失效缓存,避免选项/节点/日志库切换写入后 `/api/v1/config/public` 仍返回旧值。
|
||||
- 已部署库启动时把历史 `goose_db_version` 一次性写入 `w_schema_versions`(`openflare/legacy` 与 `server`),不再重跑 76 条混合迁移;全新安装只创建当前的 `of_*` 业务表。ClickHouse 继续只升级节点访问/可观测相关表,用户访问日志表改由上游负责。
|
||||
## [v3.5.4] - 2026-08-29
|
||||
|
||||
### ✨ 新功能
|
||||
|
||||
Reference in New Issue
Block a user