feat(pages): 支持 Remote 部署源同步

新增部署源配置与运行态模型、安全下载、租约续期、原子激活和失败补偿。

接入内部任务与脱敏前端交互,并阻止数据库 Trace 和日志展开敏感查询参数。
This commit is contained in:
deqiying
2026-07-19 17:36:51 +08:00
parent 4e8ec23264
commit 38b0516937
58 changed files with 9858 additions and 1075 deletions
+25 -18
View File
@@ -18,22 +18,23 @@ const (
// PagesProject OpenFlare Pages 静态托管项目。
type PagesProject struct {
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
Name string `json:"name" gorm:"size:255;not null"`
Slug string `json:"slug" gorm:"uniqueIndex;size:128;not null"`
Description string `json:"description" gorm:"type:text;not null;default:''"`
Enabled bool `json:"enabled" gorm:"not null;default:true"`
SPAFallbackEnabled bool `json:"spa_fallback_enabled" gorm:"not null;default:false"`
SPAFallbackPath string `json:"spa_fallback_path" gorm:"size:512;not null;default:'/index.html'"`
APIProxyEnabled bool `json:"api_proxy_enabled" gorm:"not null;default:false"`
APIProxyPath string `json:"api_proxy_path" gorm:"size:255;not null;default:''"`
APIProxyPass string `json:"api_proxy_pass" gorm:"size:2048;not null;default:''"`
APIProxyRewrite string `json:"api_proxy_rewrite" gorm:"size:255;not null;default:''"`
ActiveDeploymentID *uint `json:"active_deployment_id" gorm:"index"`
RootDir string `json:"root_dir" gorm:"size:512;not null;default:''"`
EntryFile string `json:"entry_file" gorm:"size:512;not null;default:'index.html'"`
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
Name string `json:"name" gorm:"size:255;not null"`
Slug string `json:"slug" gorm:"uniqueIndex;size:128;not null"`
Description string `json:"description" gorm:"type:text;not null;default:''"`
Enabled bool `json:"enabled" gorm:"not null;default:true"`
SPAFallbackEnabled bool `json:"spa_fallback_enabled" gorm:"not null;default:false"`
SPAFallbackPath string `json:"spa_fallback_path" gorm:"size:512;not null;default:'/index.html'"`
APIProxyEnabled bool `json:"api_proxy_enabled" gorm:"not null;default:false"`
APIProxyPath string `json:"api_proxy_path" gorm:"size:255;not null;default:''"`
APIProxyPass string `json:"api_proxy_pass" gorm:"size:2048;not null;default:''"`
APIProxyRewrite string `json:"api_proxy_rewrite" gorm:"size:255;not null;default:''"`
ActiveDeploymentID *uint `json:"active_deployment_id" gorm:"index"`
RootDir string `json:"root_dir" gorm:"size:512;not null;default:''"`
EntryFile string `json:"entry_file" gorm:"size:512;not null;default:'index.html'"`
ContentConfigVersion int `json:"-" gorm:"not null;default:0"`
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
}
// TableName 表名。
@@ -44,8 +45,8 @@ func (PagesProject) TableName() string {
// PagesDeployment OpenFlare Pages 不可变部署记录。
type PagesDeployment struct {
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
ProjectID uint `json:"project_id" gorm:"not null;index"`
DeploymentNumber int `json:"deployment_number" gorm:"not null"`
ProjectID uint `json:"project_id" gorm:"not null;index;uniqueIndex:idx_of_pages_deployments_project_number,priority:1;uniqueIndex:idx_of_pages_deployments_source_revision,priority:1,where:source_identity IS NOT NULL AND source_revision IS NOT NULL"`
DeploymentNumber int `json:"deployment_number" gorm:"not null;uniqueIndex:idx_of_pages_deployments_project_number,priority:2"`
Checksum string `json:"checksum" gorm:"size:64;not null;index"`
Status string `json:"status" gorm:"size:32;not null;default:'uploaded';index"`
UploadID uint64 `json:"upload_id,string" gorm:"not null;default:0;index"`
@@ -53,6 +54,12 @@ type PagesDeployment struct {
FileCount int `json:"file_count" gorm:"not null;default:0"`
TotalSize int64 `json:"total_size" gorm:"not null;default:0"`
CreatedBy string `json:"created_by" gorm:"size:64;not null;default:''"`
SourceType string `json:"source_type" gorm:"size:32;not null;default:''"`
SourceIdentity *string `json:"-" gorm:"type:char(64);uniqueIndex:idx_of_pages_deployments_source_revision,priority:2,where:source_identity IS NOT NULL AND source_revision IS NOT NULL"`
SourceRevision *string `json:"-" gorm:"type:char(64);uniqueIndex:idx_of_pages_deployments_source_revision,priority:3,where:source_identity IS NOT NULL AND source_revision IS NOT NULL"`
SourceLabel string `json:"source_label" gorm:"size:255;not null;default:''"`
SourceMeta string `json:"-" gorm:"type:text;not null;default:''"`
TriggerType string `json:"trigger_type" gorm:"size:32;not null;default:''"`
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
ActivatedAt *time.Time `json:"activated_at"`
}
+59
View File
@@ -0,0 +1,59 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package model
import "time"
// PagesProjectSource 保存 Pages 项目的持久部署源配置。
//
// RemoteURL 可能包含签名参数,禁止直接序列化 model;对外接口必须映射到
// pages 包内的脱敏 source view。
type PagesProjectSource struct {
ID uint `json:"-" gorm:"primaryKey;autoIncrement"`
ProjectID uint `json:"-" gorm:"not null;uniqueIndex:idx_of_pages_project_sources_project_id"`
SourceType string `json:"-" gorm:"size:32;not null;default:''"`
RemoteURL string `json:"-" gorm:"type:text;not null;default:''"`
RemoteNetworkPolicy string `json:"-" gorm:"size:32;not null;default:''"`
GitHubRepository string `json:"-" gorm:"column:github_repository;size:255;not null;default:''"`
ReleaseSelector string `json:"-" gorm:"size:16;not null;default:''"`
ReleaseTag string `json:"-" gorm:"size:255;not null;default:''"`
AssetName string `json:"-" gorm:"size:255;not null;default:''"`
AutoUpdateEnabled bool `json:"-" gorm:"not null;default:false"`
CheckIntervalMinutes int `json:"-" gorm:"not null;default:0"`
ConfigVersion int `json:"-" gorm:"not null;default:0"`
SourceIdentity string `json:"-" gorm:"type:char(64);not null;default:''"`
CreatedAt time.Time `json:"-" gorm:"autoCreateTime"`
UpdatedAt time.Time `json:"-" gorm:"autoUpdateTime"`
}
// TableName 返回 Pages 项目部署源配置表名。
func (PagesProjectSource) TableName() string {
return "of_pages_project_sources"
}
// PagesProjectSourceRuntime 保存 Pages 项目部署源的可变运行态。
//
// Runtime 不冗余 project_id;调用方通过 SourceID 关联配置,并在最终提交时
// 同时校验 source config version 与 project content config version。
type PagesProjectSourceRuntime struct {
SourceID uint `json:"-" gorm:"primaryKey;autoIncrement:false"`
ETag string `json:"-" gorm:"column:etag;size:512;not null;default:''"`
LastSeenRevision string `json:"-" gorm:"type:char(64);not null;default:''"`
LastSeenDetail string `json:"-" gorm:"type:text;not null;default:''"`
LastAppliedRevision string `json:"-" gorm:"type:char(64);not null;default:''"`
LastAppliedDetail string `json:"-" gorm:"type:text;not null;default:''"`
SyncStatus string `json:"-" gorm:"size:32;not null;default:''"`
LastError string `json:"-" gorm:"type:text;not null;default:''"`
LastCheckedAt *time.Time `json:"-"`
LastSyncedAt *time.Time `json:"-"`
NextCheckAt *time.Time `json:"-" gorm:"index:idx_of_pages_project_source_runtime_next_check_at"`
LeaseExpiresAt *time.Time `json:"-"`
LeaseToken string `json:"-" gorm:"size:64;not null;default:''"`
UpdatedAt time.Time `json:"-" gorm:"autoUpdateTime"`
}
// TableName 返回 Pages 项目部署源运行态表名。
func (PagesProjectSourceRuntime) TableName() string {
return "of_pages_project_source_runtime"
}
@@ -0,0 +1,80 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package model
import (
"encoding/json"
"strings"
"testing"
"github.com/glebarez/sqlite"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
)
func TestPagesSourceModelsMatchMigrationSchema(t *testing.T) {
gormDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
DisableForeignKeyConstraintWhenMigrating: true,
})
require.NoError(t, err)
require.NoError(t, gormDB.AutoMigrate(
&PagesProject{},
&PagesDeployment{},
&PagesProjectSource{},
&PagesProjectSourceRuntime{},
))
assert.Equal(t, "of_pages_project_sources", (PagesProjectSource{}).TableName())
assert.Equal(t, "of_pages_project_source_runtime", (PagesProjectSourceRuntime{}).TableName())
assert.True(t, gormDB.Migrator().HasColumn(&PagesProjectSource{}, "github_repository"))
assert.False(t, gormDB.Migrator().HasColumn(&PagesProjectSource{}, "git_hub_repository"))
assert.True(t, gormDB.Migrator().HasColumn(&PagesProjectSourceRuntime{}, "etag"))
assert.False(t, gormDB.Migrator().HasColumn(&PagesProjectSourceRuntime{}, "e_tag"))
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 TestPagesSourceModelsDoNotSerializeSecretsOrFencingState(t *testing.T) {
sourceJSON, err := json.Marshal(PagesProjectSource{
ID: 1,
ProjectID: 2,
RemoteURL: "https://example.com/site.zip?token=secret",
ConfigVersion: 3,
SourceIdentity: strings.Repeat("a", 64),
})
require.NoError(t, err)
assert.JSONEq(t, `{}`, string(sourceJSON))
runtimeJSON, err := json.Marshal(PagesProjectSourceRuntime{
SourceID: 1,
ETag: `"secret-etag"`,
LeaseToken: "secret-lease",
})
require.NoError(t, err)
assert.JSONEq(t, `{}`, string(runtimeJSON))
identity := strings.Repeat("b", 64)
revision := strings.Repeat("c", 64)
deploymentJSON, err := json.Marshal(PagesDeployment{
SourceType: "remote_url",
SourceIdentity: &identity,
SourceRevision: &revision,
SourceLabel: "site.zip",
SourceMeta: `{"provider":"remote_url","private":"secret"}`,
TriggerType: "manual_sync",
})
require.NoError(t, err)
assert.NotContains(t, string(deploymentJSON), identity)
assert.NotContains(t, string(deploymentJSON), revision)
assert.NotContains(t, string(deploymentJSON), "private")
assert.Contains(t, string(deploymentJSON), `"source_type":"remote_url"`)
assert.Contains(t, string(deploymentJSON), `"source_label":"site.zip"`)
assert.Contains(t, string(deploymentJSON), `"trigger_type":"manual_sync"`)
}