[修复] 更新数据库字段类型为text,添加消息截断逻辑以支持更长的消息内容

This commit is contained in:
ryan
2026-03-18 22:57:14 +08:00
parent 1fbe156a7c
commit 4945b8b44f
13 changed files with 204 additions and 60 deletions
+2 -1
View File
@@ -84,10 +84,11 @@ OpenResty 性能参数与缓存参数继续统一保存在 `Option` 表。当前
这类参数必须以结构化方式校验、保存并参与版本渲染。
* `OpenRestyResolvers` 由管理端性能页面维护,支持填写多个 DNS 服务器 IP;留空时不额外生成 `resolver` 指令。
* 规则内如使用多主机名上游做负载均衡,或希望主机名上游渲染为带 keepalive 的 named `upstream`,必须先配置 `OpenRestyResolvers`;否则会回退为 direct `proxy_pass`,或在不安全的多上游场景直接阻止发布。
* `OpenRestyCacheEnabled` 用于启用缓存基础设施与全局默认参数;实际是否缓存、按 URL / 后缀 / 路径等命中策略由各条 `proxy_routes` 单独决定,不再默认对所有规则开启缓存。
* 默认缓存 Key 为 `$scheme$host$request_uri`,更贴近代理域名维度;如需按其他维度命中,可在性能页显式覆盖。
* 默认 `keepalive_timeout` 为 `20` 秒,默认 `proxy_connect_timeout` 为 `3` 秒,优先兼顾资源占用与回源失败切换速度。
* 默认事件模型为 `epoll`,并默认开启 `multi_accept`;HTTPS 监听默认附带 `reuseport`,以改善多 worker 下的连接分发。
* 默认事件模型为 `epoll`,并默认开启 `multi_accept`;HTTPS 监听默认使用独立 `http2 on;` 指令,避免新版 Nginx/OpenResty 对 `listen ... http2` 的弃用告警。
### 1.5 前端构建环境变量
| 环境变量 | 作用 | 默认值 |
+1
View File
@@ -121,6 +121,7 @@ Origin
* 一个域名只对应一条 `proxy_routes` 规则
* `proxy_routes` 至少包含一个上游地址;为兼容历史数据保留 `origin_url` 主上游字段,也允许在同一规则内补充多个上游做负载均衡
* 单上游或多上游只有在目标可静态解析,或已配置 `OpenRestyResolvers` 时,才会渲染为带 keepalive 的 named `upstream`
* `proxy_routes.origin_host` 为可选字段,用于回源时覆盖 `Host` 请求头;未设置时默认透传访问域名
* `proxy_routes.domain` 必须唯一
* 所有上游地址都必须为合法 `http://` 或 `https://`
+1
View File
@@ -116,6 +116,7 @@
* 不新增平台化对象,除非设计文档明确要求
* `proxy_routes` 维持一条域名对应一条规则;规则内允许保存一个或多个上游地址用于负载均衡,但不引入独立 `origin_pool`
* 生成 named `upstream` 时必须区分可静态解析与依赖运行时 resolver 的主机名;禁止在无 `OpenRestyResolvers` 的情况下为多主机名上游生成会导致 OpenResty 启动失败的配置
* `proxy_routes.origin_host` 为可选字段,仅用于覆盖回源 `Host` 请求头,不引入新的平台化对象
* `config_versions` 必须保存完整快照与渲染结果
* 全局同时只能有一个激活版本
+1 -1
View File
@@ -17,7 +17,7 @@ type ApplyLog struct {
NodeID string `json:"node_id" gorm:"index;size:64;not null"`
Version string `json:"version" gorm:"size:32;not null"`
Result string `json:"result" gorm:"size:32;not null"`
Message string `json:"message" gorm:"size:1024"`
Message string `json:"message" gorm:"type:text"`
Checksum string `json:"checksum" gorm:"size:64;not null;default:''"`
MainConfigChecksum string `json:"main_config_checksum" gorm:"size:64;not null;default:''"`
RouteConfigChecksum string `json:"route_config_checksum" gorm:"size:64;not null;default:''"`
+30
View File
@@ -126,6 +126,33 @@ func autoMigrateAll(db *gorm.DB) error {
return nil
}
func migrateTextColumns(db *gorm.DB, backend string) error {
if backend != "postgres" {
return nil
}
type textColumn struct {
model any
table string
column string
}
columns := []textColumn{
{model: &Node{}, table: "nodes", column: "openresty_message"},
{model: &Node{}, table: "nodes", column: "last_error"},
{model: &ApplyLog{}, table: "apply_logs", column: "message"},
{model: &NodeHealthEvent{}, table: "node_health_events", column: "message"},
}
for _, item := range columns {
if !db.Migrator().HasTable(item.model) || !db.Migrator().HasColumn(item.model, item.column) {
continue
}
sql := fmt.Sprintf(`ALTER TABLE "%s" ALTER COLUMN "%s" TYPE text`, item.table, item.column)
if err := db.Exec(sql).Error; err != nil {
return fmt.Errorf("migrate column %s.%s to text failed: %w", item.table, item.column, err)
}
}
return nil
}
func isDatabaseEmpty(db *gorm.DB) (bool, error) {
for _, item := range registeredModels() {
var count int64
@@ -272,6 +299,9 @@ func InitDB() (err error) {
if err = autoMigrateAll(db); err != nil {
return err
}
if err = migrateTextColumns(db, backend); err != nil {
return err
}
if err = migrateSQLiteDataIfNeeded(db, backend); err != nil {
return err
}
+2 -2
View File
@@ -20,11 +20,11 @@ type Node struct {
AgentVersion string `json:"agent_version" gorm:"size:64;not null"`
NginxVersion string `json:"nginx_version" gorm:"size:64"`
OpenrestyStatus string `json:"openresty_status" gorm:"size:16;not null;default:'unknown'"`
OpenrestyMessage string `json:"openresty_message" gorm:"size:2048"`
OpenrestyMessage string `json:"openresty_message" gorm:"type:text"`
Status string `json:"status" gorm:"size:16;not null;default:'offline'"`
CurrentVersion string `json:"current_version" gorm:"size:32"`
LastSeenAt time.Time `json:"last_seen_at"`
LastError string `json:"last_error" gorm:"size:1024"`
LastError string `json:"last_error" gorm:"type:text"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
+1 -1
View File
@@ -8,7 +8,7 @@ type NodeHealthEvent struct {
EventType string `json:"event_type" gorm:"index;size:64;not null"`
Severity string `json:"severity" gorm:"size:16;not null"`
Status string `json:"status" gorm:"index;size:16;not null"`
Message string `json:"message" gorm:"size:2048"`
Message string `json:"message" gorm:"type:text"`
FirstTriggeredAt time.Time `json:"first_triggered_at" gorm:"index"`
LastTriggeredAt time.Time `json:"last_triggered_at" gorm:"index"`
ReportedAt time.Time `json:"reported_at" gorm:"index"`
+12 -9
View File
@@ -45,8 +45,8 @@ func TestPhase1PublishLifecycle(t *testing.T) {
createBody := map[string]any{
"domain": "app.example.com",
"origin_url": "https://origin-a.internal",
"upstreams": []string{"https://origin-a-backup.internal"},
"origin_url": "https://10.0.0.11:8443",
"upstreams": []string{"https://10.0.0.12:8443"},
"origin_host": "origin-a.internal",
"enabled": true,
"cache_enabled": true,
@@ -66,7 +66,7 @@ func TestPhase1PublishLifecycle(t *testing.T) {
if !createdRoute.CacheEnabled || createdRoute.CachePolicy != "path_prefix" {
t.Fatalf("expected route cache settings to persist, got %+v", createdRoute)
}
if !strings.Contains(createdRoute.Upstreams, "origin-a-backup.internal") {
if !strings.Contains(createdRoute.Upstreams, "10.0.0.12:8443") {
t.Fatalf("expected route upstream list to persist, got %s", createdRoute.Upstreams)
}
if !strings.Contains(createdRoute.CacheRules, "/assets") {
@@ -117,8 +117,8 @@ func TestPhase1PublishLifecycle(t *testing.T) {
updateBody := map[string]any{
"domain": "app.example.com",
"origin_url": "https://origin-b.internal",
"upstreams": []string{"https://origin-b-backup.internal"},
"origin_url": "https://10.0.0.21:8443",
"upstreams": []string{"https://10.0.0.22:8443"},
"origin_host": "origin-b.internal",
"enabled": true,
"cache_enabled": true,
@@ -129,7 +129,7 @@ func TestPhase1PublishLifecycle(t *testing.T) {
routePath := "/api/proxy-routes/" + toString(createdRoute.ID)
resp = performJSONRequest(t, engine, token, http.MethodPost, routePath+"/update", updateBody)
decodeResponseData(t, resp, &createdRoute)
if createdRoute.OriginURL != "https://origin-b.internal" {
if createdRoute.OriginURL != "https://10.0.0.21:8443" {
t.Fatalf("unexpected updated route origin: %s", createdRoute.OriginURL)
}
if createdRoute.OriginHost != "origin-b.internal" {
@@ -138,7 +138,7 @@ func TestPhase1PublishLifecycle(t *testing.T) {
if createdRoute.CachePolicy != "path_exact" || !strings.Contains(createdRoute.CacheRules, "/robots.txt") {
t.Fatalf("expected updated route cache rules to persist, got %+v", createdRoute)
}
if !strings.Contains(createdRoute.Upstreams, "origin-b-backup.internal") {
if !strings.Contains(createdRoute.Upstreams, "10.0.0.22:8443") {
t.Fatalf("expected updated route upstream list to persist, got %s", createdRoute.Upstreams)
}
@@ -349,8 +349,11 @@ func TestPhase1HTTPSAndCertificateImportLifecycle(t *testing.T) {
if !strings.Contains(version.MainConfig, "include __OPENFLARE_ROUTE_CONFIG__;") {
t.Fatal("expected active config to render managed main config")
}
if !strings.Contains(version.RenderedConfig, "listen 443 ssl http2 reuseport;") {
t.Fatal("expected active config to render https listener with http2, reuseport enabled")
if !strings.Contains(version.RenderedConfig, "listen 443 ssl;") {
t.Fatal("expected active config to render https ssl listener")
}
if !strings.Contains(version.RenderedConfig, "http2 on;") {
t.Fatal("expected active config to render dedicated http2 directive")
}
if !strings.Contains(version.RenderedConfig, "return 301 https://$host$request_uri;") {
t.Fatal("expected active config to render redirect server")
+12
View File
@@ -254,6 +254,7 @@ func ReportApplyLog(payload ApplyLogPayload) (*model.ApplyLog, error) {
payload.Checksum = strings.TrimSpace(payload.Checksum)
payload.MainConfigChecksum = strings.TrimSpace(payload.MainConfigChecksum)
payload.RouteConfigChecksum = strings.TrimSpace(payload.RouteConfigChecksum)
payload.Message = truncateForDatabase(payload.Message, 16000)
if payload.NodeID == "" {
return nil, errors.New("node_id 不能为空")
}
@@ -339,6 +340,17 @@ func ListNodeViews() ([]*NodeView, error) {
return views, nil
}
func truncateForDatabase(value string, max int) string {
if max <= 0 {
return ""
}
runes := []rune(strings.TrimSpace(value))
if len(runes) <= max {
return string(runes)
}
return string(runes[:max])
}
const (
defaultApplyLogPageSize = 20
maxApplyLogPageSize = 200
+54 -11
View File
@@ -83,10 +83,15 @@ type routeCacheConfig struct {
type routeUpstreamConfig struct {
Name string
Scheme string
Addresses []string
Servers []routeUpstreamServer
UsesNamedUpstream bool
}
type routeUpstreamServer struct {
Address string
Resolve bool
}
type openRestyConfigSnapshot struct {
WorkerProcesses string `json:"worker_processes"`
WorkerConnections int `json:"worker_connections"`
@@ -671,6 +676,9 @@ func renderRouteConfig(routes []*model.ProxyRoute, cfg openRestyConfigSnapshot)
if err != nil {
return "", nil, fmt.Errorf("路由 %s 上游配置无效", route.Domain)
}
if err := validateRenderableUpstreams(route.Domain, upstreams, cfg); err != nil {
return "", nil, err
}
cacheRules, err := decodeStoredCacheRules(route.CacheRules)
if err != nil {
return "", nil, fmt.Errorf("路由 %s 缓存规则无效", route.Domain)
@@ -852,7 +860,7 @@ func renderHTTPRedirectServer(domain string) string {
func renderHTTPSServer(domain string, originURL string, originHost string, certificateID uint, customHeaders []ProxyRouteCustomHeaderInput, cacheConfig routeCacheConfig, upstreamConfig routeUpstreamConfig, cfg openRestyConfigSnapshot) string {
certPath := fmt.Sprintf("%s/%s", nginxCertDirPlaceholder, certificateCertFileName(certificateID))
keyPath := fmt.Sprintf("%s/%s", nginxCertDirPlaceholder, certificateKeyFileName(certificateID))
return fmt.Sprintf("server {\n listen 443 ssl http2 reuseport;\n server_name %s;\n ssl_certificate %s;\n ssl_certificate_key %s;\n\n location / {\n%s%s%s }\n}\n\n", domain, certPath, keyPath, renderProxyHeaderBlock(originURL, originHost, customHeaders, upstreamConfig), renderRouteCacheBlock(cacheConfig, cfg), renderProxyPassBlock(originURL, upstreamConfig, cfg))
return fmt.Sprintf("server {\n listen 443 ssl;\n http2 on;\n server_name %s;\n ssl_certificate %s;\n ssl_certificate_key %s;\n\n location / {\n%s%s%s }\n}\n\n", domain, certPath, keyPath, renderProxyHeaderBlock(originURL, originHost, customHeaders, upstreamConfig), renderRouteCacheBlock(cacheConfig, cfg), renderProxyPassBlock(originURL, upstreamConfig, cfg))
}
func renderConnectionUpgradeMap() string {
@@ -997,23 +1005,27 @@ func buildRouteUpstreamConfig(route *model.ProxyRoute, upstreams []string, cfg o
if err != nil || parsed.Host == "" || parsed.Scheme == "" {
return routeUpstreamConfig{}
}
if shouldUseRuntimeResolver(upstreams[0], cfg.Resolvers) {
return routeUpstreamConfig{}
}
if strings.TrimSpace(parsed.EscapedPath()) != "" && strings.TrimSpace(parsed.EscapedPath()) != "/" {
return routeUpstreamConfig{}
}
if parsed.RawQuery != "" {
return routeUpstreamConfig{}
}
server := routeUpstreamServer{Address: parsed.Host}
if !isIPAddressHostname(parsed.Hostname()) {
if strings.TrimSpace(cfg.Resolvers) == "" {
return routeUpstreamConfig{}
}
server.Resolve = true
}
return routeUpstreamConfig{
Name: buildRouteUpstreamName(route),
Scheme: parsed.Scheme,
Addresses: []string{parsed.Host},
Servers: []routeUpstreamServer{server},
UsesNamedUpstream: true,
}
}
addresses := make([]string, 0, len(upstreams))
servers := make([]routeUpstreamServer, 0, len(upstreams))
var scheme string
for _, upstream := range upstreams {
parsed, err := url.Parse(strings.TrimSpace(upstream))
@@ -1031,12 +1043,16 @@ func buildRouteUpstreamConfig(route *model.ProxyRoute, upstreams []string, cfg o
} else if scheme != parsed.Scheme {
return routeUpstreamConfig{}
}
addresses = append(addresses, parsed.Host)
server := routeUpstreamServer{Address: parsed.Host}
if !isIPAddressHostname(parsed.Hostname()) {
server.Resolve = true
}
servers = append(servers, server)
}
return routeUpstreamConfig{
Name: buildRouteUpstreamName(route),
Scheme: scheme,
Addresses: addresses,
Servers: servers,
UsesNamedUpstream: true,
}
}
@@ -1064,13 +1080,40 @@ func buildRouteUpstreamName(route *model.ProxyRoute) string {
func renderNamedUpstreamBlock(upstreamConfig routeUpstreamConfig) string {
var builder strings.Builder
builder.WriteString(fmt.Sprintf("upstream %s {\n", upstreamConfig.Name))
for _, address := range upstreamConfig.Addresses {
builder.WriteString(fmt.Sprintf(" server %s max_fails=3 fail_timeout=10s;\n", address))
for _, server := range upstreamConfig.Servers {
if server.Resolve {
builder.WriteString(fmt.Sprintf(" server %s resolve max_fails=3 fail_timeout=10s;\n", server.Address))
continue
}
builder.WriteString(fmt.Sprintf(" server %s max_fails=3 fail_timeout=10s;\n", server.Address))
}
builder.WriteString(" keepalive 128;\n}\n\n")
return builder.String()
}
func validateRenderableUpstreams(domain string, upstreams []string, cfg openRestyConfigSnapshot) error {
if len(upstreams) <= 1 {
return nil
}
if strings.TrimSpace(cfg.Resolvers) != "" {
return nil
}
for _, upstream := range upstreams {
parsed, err := url.Parse(strings.TrimSpace(upstream))
if err != nil || parsed.Hostname() == "" {
return fmt.Errorf("路由 %s 上游配置无效", domain)
}
if !isIPAddressHostname(parsed.Hostname()) {
return fmt.Errorf("路由 %s 的多上游主机名需要先配置 OpenRestyResolvers", domain)
}
}
return nil
}
func isIPAddressHostname(host string) bool {
return net.ParseIP(strings.TrimSpace(host)) != nil
}
func shouldUseRuntimeResolver(originURL string, resolvers string) bool {
if strings.TrimSpace(resolvers) == "" {
return false
+78 -29
View File
@@ -81,8 +81,11 @@ func TestCreateTLSCertificateAndRenderHTTPSConfig(t *testing.T) {
if strings.Contains(result.Version.MainConfig, "allow 127.0.0.1;") {
t.Fatal("expected main config to avoid hard-coded allow rules on observability server")
}
if !strings.Contains(result.Version.RenderedConfig, "listen 443 ssl http2 reuseport;") {
t.Fatal("expected rendered config to include https server block with http2 and reuseport enabled")
if !strings.Contains(result.Version.RenderedConfig, "listen 443 ssl;") {
t.Fatal("expected rendered config to include https ssl listener")
}
if !strings.Contains(result.Version.RenderedConfig, "http2 on;") {
t.Fatal("expected rendered config to enable http2 with dedicated directive")
}
if strings.Contains(result.Version.RenderedConfig, `if ($host != "app.example.com") {`) {
t.Fatal("expected rendered config to avoid per-route host guard")
@@ -153,14 +156,8 @@ func TestPublishConfigVersionRendersCustomHeaders(t *testing.T) {
if !strings.Contains(result.Version.RenderedConfig, "proxy_set_header Connection $connection_upgrade;") {
t.Fatal("expected rendered config to use normalized websocket connection header")
}
if !strings.Contains(result.Version.RenderedConfig, "upstream backend_custom_example_com_1 {") {
t.Fatal("expected rendered config to define named upstream for simple origins")
}
if !strings.Contains(result.Version.RenderedConfig, "keepalive 128;") {
t.Fatal("expected rendered config to enable upstream keepalive")
}
if !strings.Contains(result.Version.RenderedConfig, "proxy_pass https://backend_custom_example_com_1;") {
t.Fatal("expected rendered config to proxy through named upstream when no resolver is required")
if !strings.Contains(result.Version.RenderedConfig, "proxy_pass https://origin.internal;") {
t.Fatal("expected hostname origin without resolver to keep direct proxy_pass")
}
if strings.Contains(result.Version.RenderedConfig, "proxy_pass $openflare_upstream$request_uri;") {
t.Fatal("expected rendered config to avoid runtime-resolved proxy_pass when no resolvers are configured")
@@ -242,8 +239,8 @@ func TestPublishConfigVersionRendersRouteLevelCachePolicy(t *testing.T) {
if strings.Count(result.Version.RenderedConfig, "proxy_cache openflare_cache;") != 1 {
t.Fatal("expected only cache-enabled route to include proxy_cache directive")
}
if !strings.Contains(result.Version.RenderedConfig, "proxy_pass https://backend_static_example_com_1;") {
t.Fatal("expected cache-enabled route to proxy through named upstream")
if !strings.Contains(result.Version.RenderedConfig, "proxy_pass https://origin.internal;") {
t.Fatal("expected cache-enabled hostname route without resolver to keep direct proxy_pass")
}
if !strings.Contains(result.Version.SnapshotJSON, `"cache_enabled":true`) {
t.Fatal("expected snapshot to include route cache toggle")
@@ -258,15 +255,15 @@ func TestPublishConfigVersionRendersMultipleUpstreams(t *testing.T) {
route, err := CreateProxyRoute(ProxyRouteInput{
Domain: "lb.example.com",
OriginURL: "http://c1:39010",
Upstreams: []string{"http://c2:39010", "http://c3:39010"},
OriginURL: "http://10.0.0.11:39010",
Upstreams: []string{"http://10.0.0.12:39010", "http://10.0.0.13:39010"},
Enabled: true,
OriginHost: "lb.example.com",
})
if err != nil {
t.Fatalf("CreateProxyRoute failed: %v", err)
}
if !strings.Contains(route.Upstreams, "c2:39010") {
if !strings.Contains(route.Upstreams, "10.0.0.12:39010") {
t.Fatalf("expected route upstreams to persist, got %s", route.Upstreams)
}
@@ -277,26 +274,45 @@ func TestPublishConfigVersionRendersMultipleUpstreams(t *testing.T) {
if !strings.Contains(result.Version.RenderedConfig, "upstream backend_lb_example_com_1 {") {
t.Fatal("expected rendered config to define upstream block for load balancing route")
}
if strings.Count(result.Version.RenderedConfig, "server c") < 3 {
if strings.Count(result.Version.RenderedConfig, "max_fails=3 fail_timeout=10s;") < 3 {
t.Fatal("expected rendered config to include every upstream server")
}
if !strings.Contains(result.Version.RenderedConfig, "server c1:39010 max_fails=3 fail_timeout=10s;") {
if !strings.Contains(result.Version.RenderedConfig, "server 10.0.0.11:39010 max_fails=3 fail_timeout=10s;") {
t.Fatal("expected rendered config to include primary upstream server")
}
if !strings.Contains(result.Version.RenderedConfig, "server c2:39010 max_fails=3 fail_timeout=10s;") {
if !strings.Contains(result.Version.RenderedConfig, "server 10.0.0.12:39010 max_fails=3 fail_timeout=10s;") {
t.Fatal("expected rendered config to include secondary upstream server")
}
if !strings.Contains(result.Version.RenderedConfig, "server c3:39010 max_fails=3 fail_timeout=10s;") {
if !strings.Contains(result.Version.RenderedConfig, "server 10.0.0.13:39010 max_fails=3 fail_timeout=10s;") {
t.Fatal("expected rendered config to include tertiary upstream server")
}
if !strings.Contains(result.Version.RenderedConfig, "proxy_pass http://backend_lb_example_com_1;") {
t.Fatal("expected rendered config to proxy through load balancing upstream")
}
if !strings.Contains(result.Version.SnapshotJSON, `"upstreams":["http://c1:39010","http://c2:39010","http://c3:39010"]`) {
if !strings.Contains(result.Version.SnapshotJSON, `"upstreams":["http://10.0.0.11:39010","http://10.0.0.12:39010","http://10.0.0.13:39010"]`) {
t.Fatal("expected snapshot to include upstream list")
}
}
func TestPublishConfigVersionRejectsHostnameLoadBalancingWithoutResolvers(t *testing.T) {
setupServiceTestDB(t)
_, err := CreateProxyRoute(ProxyRouteInput{
Domain: "hostname-lb.example.com",
OriginURL: "http://c1:39010",
Upstreams: []string{"http://c2:39010"},
Enabled: true,
})
if err != nil {
t.Fatalf("CreateProxyRoute failed: %v", err)
}
_, err = PublishConfigVersion("root")
if err == nil || !strings.Contains(err.Error(), "多上游主机名需要先配置 OpenRestyResolvers") {
t.Fatalf("expected hostname load balancing publish validation error, got %v", err)
}
}
func TestPublishConfigVersionOverridesOriginHostHeader(t *testing.T) {
setupServiceTestDB(t)
@@ -323,11 +339,8 @@ func TestPublishConfigVersionOverridesOriginHostHeader(t *testing.T) {
if !strings.Contains(result.Version.RenderedConfig, `proxy_ssl_name "git.arctel.net";`) {
t.Fatal("expected rendered config to set proxy ssl name from origin host override")
}
if !strings.Contains(result.Version.RenderedConfig, "upstream backend_git_arctel_de_1 {") {
t.Fatal("expected rendered config to define named upstream for static hostname origins")
}
if !strings.Contains(result.Version.RenderedConfig, "proxy_pass https://backend_git_arctel_de_1;") {
t.Fatal("expected rendered config to proxy through named upstream for hostname origin when resolvers are blank")
if !strings.Contains(result.Version.RenderedConfig, "proxy_pass https://git.arctel.net;") {
t.Fatal("expected rendered config to keep direct proxy_pass for hostname origin when resolvers are blank")
}
if !strings.Contains(result.Version.SnapshotJSON, `"origin_host":"git.arctel.net"`) {
t.Fatal("expected snapshot to include origin_host override")
@@ -342,7 +355,7 @@ func TestPublishConfigVersionUsesRuntimeResolverWhenConfigured(t *testing.T) {
_, err := CreateProxyRoute(ProxyRouteInput{
Domain: "resolver.example.com",
OriginURL: "https://origin.internal",
OriginURL: "https://origin.internal/api",
Enabled: true,
})
if err != nil {
@@ -357,13 +370,46 @@ func TestPublishConfigVersionUsesRuntimeResolverWhenConfigured(t *testing.T) {
t.Fatal("expected main config to render configured resolver directive")
}
if strings.Contains(result.Version.RenderedConfig, "upstream backend_resolver_example_com_1 {") {
t.Fatal("expected runtime-resolved origin to avoid named upstream block")
t.Fatal("expected hostname origin with base path to avoid named upstream block")
}
if !strings.Contains(result.Version.RenderedConfig, `set $openflare_upstream "https://origin.internal";`) {
t.Fatal("expected rendered config to use runtime upstream variable when resolvers are configured")
}
if !strings.Contains(result.Version.RenderedConfig, "proxy_pass $openflare_upstream$request_uri;") {
t.Fatal("expected rendered config to proxy via runtime-resolved upstream variable when resolvers are configured")
if !strings.Contains(result.Version.RenderedConfig, `set $openflare_upstream_base_path "/api";`) {
t.Fatal("expected rendered config to preserve origin base path for runtime-resolved origin")
}
if !strings.Contains(result.Version.RenderedConfig, "proxy_pass $openflare_upstream$openflare_upstream_base_path$request_uri;") {
t.Fatal("expected rendered config to proxy via runtime-resolved upstream variable and base path when resolvers are configured")
}
}
func TestPublishConfigVersionUsesNamedUpstreamForHostnameOriginsWhenResolversConfigured(t *testing.T) {
setupServiceTestDB(t)
if err := model.UpdateOption("OpenRestyResolvers", "1.1.1.1"); err != nil {
t.Fatalf("UpdateOption OpenRestyResolvers failed: %v", err)
}
_, err := CreateProxyRoute(ProxyRouteInput{
Domain: "resolver-upstream.example.com",
OriginURL: "https://origin.internal",
Enabled: true,
})
if err != nil {
t.Fatalf("CreateProxyRoute failed: %v", err)
}
result, err := PublishConfigVersion("root")
if err != nil {
t.Fatalf("PublishConfigVersion failed: %v", err)
}
if !strings.Contains(result.Version.RenderedConfig, "upstream backend_resolver_upstream_example_com_1 {") {
t.Fatal("expected rendered config to define named upstream for hostname origin when resolvers are configured")
}
if !strings.Contains(result.Version.RenderedConfig, "server origin.internal resolve max_fails=3 fail_timeout=10s;") {
t.Fatal("expected rendered config to mark hostname upstream server as resolve")
}
if !strings.Contains(result.Version.RenderedConfig, "proxy_pass https://backend_resolver_upstream_example_com_1;") {
t.Fatal("expected rendered config to proxy through named upstream when resolver-backed hostname upstream is safe")
}
}
@@ -396,6 +442,9 @@ func TestPublishConfigVersionKeepsDirectProxyPassForIPOrigins(t *testing.T) {
func TestPreviewConfigVersionCanDisableWebsocketHeaders(t *testing.T) {
setupServiceTestDB(t)
if err := model.UpdateOption("OpenRestyResolvers", ""); err != nil {
t.Fatalf("UpdateOption OpenRestyResolvers failed: %v", err)
}
_, err := CreateProxyRoute(ProxyRouteInput{
Domain: "ws-off.example.com",
+4 -4
View File
@@ -418,9 +418,9 @@ func normalizeAgentNodePayload(payload AgentNodePayload) AgentNodePayload {
payload.AgentVersion = strings.TrimSpace(payload.AgentVersion)
payload.NginxVersion = strings.TrimSpace(payload.NginxVersion)
payload.CurrentVersion = strings.TrimSpace(payload.CurrentVersion)
payload.LastError = strings.TrimSpace(payload.LastError)
payload.LastError = truncateForDatabase(payload.LastError, 16000)
payload.OpenrestyStatus = normalizeOpenrestyStatus(payload.OpenrestyStatus)
payload.OpenrestyMessage = strings.TrimSpace(payload.OpenrestyMessage)
payload.OpenrestyMessage = truncateForDatabase(payload.OpenrestyMessage, 16000)
return payload
}
@@ -444,11 +444,11 @@ func applyNodeRuntime(node *model.Node, payload AgentNodePayload, preserveName b
node.AgentVersion = strings.TrimSpace(payload.AgentVersion)
node.NginxVersion = strings.TrimSpace(payload.NginxVersion)
node.OpenrestyStatus = normalizeOpenrestyStatus(payload.OpenrestyStatus)
node.OpenrestyMessage = strings.TrimSpace(payload.OpenrestyMessage)
node.OpenrestyMessage = truncateForDatabase(payload.OpenrestyMessage, 16000)
node.Status = NodeStatusOnline
node.CurrentVersion = strings.TrimSpace(payload.CurrentVersion)
node.LastSeenAt = time.Now()
node.LastError = strings.TrimSpace(payload.LastError)
node.LastError = truncateForDatabase(payload.LastError, 16000)
if !node.GeoManualOverride {
applyGeoInfoFromIP(node, node.IP)
}
+6 -2
View File
@@ -271,7 +271,7 @@ func reconcileNodeHealthEvents(tx *gorm.DB, nodeID string, events []AgentNodeHea
triggeredAt := timeFromUnix(event.TriggeredAtUnix, reportedAt)
if existing, ok := activeByType[eventType]; ok {
existing.Severity = event.Severity
existing.Message = strings.TrimSpace(event.Message)
existing.Message = normalizeHealthEventMessage(event.Message)
existing.LastTriggeredAt = triggeredAt
existing.ReportedAt = reportedAt
existing.RawJSON = marshalJSON(event)
@@ -286,7 +286,7 @@ func reconcileNodeHealthEvents(tx *gorm.DB, nodeID string, events []AgentNodeHea
EventType: eventType,
Severity: event.Severity,
Status: NodeHealthEventStatusActive,
Message: strings.TrimSpace(event.Message),
Message: normalizeHealthEventMessage(event.Message),
FirstTriggeredAt: triggeredAt,
LastTriggeredAt: triggeredAt,
ReportedAt: reportedAt,
@@ -330,6 +330,10 @@ func normalizeHealthSeverity(severity string) string {
}
}
func normalizeHealthEventMessage(message string) string {
return truncateForDatabase(message, 4096)
}
func timeFromUnix(unixSeconds int64, fallback time.Time) time.Time {
if unixSeconds <= 0 {
return fallback