feat(gateway-config): 实现 K8s 服务监听和待确认配置管理基础结构
All checks were successful
Build and Push Docker / build (push) Successful in 4m28s
All checks were successful
Build and Push Docker / build (push) Successful in 4m28s
本次提交包含网关配置重构的第一阶段实现(IMPL-1, IMPL-4): ## 新增功能 ### 1. K8s 服务监听服务 (IMPL-1) - 新增 K8sServiceWatchService - 后台服务监听 Kubernetes Service 变化 - 支持 Watch API 和自动重连机制(指数退避策略) - 使用 Channel 处理事件,防止内存无限增长 - 支持集群内配置和本地 kubeconfig 两种模式 ### 2. 待确认配置管理 (IMPL-4) - 新增 PendingConfig 实体,用于存储待用户确认的配置变更 - 新增 PendingConfigType 枚举(Route, Cluster, Destination) - 新增 PendingConfigSource 枚举(K8sDiscovery, Manual) - 新增 PendingConfigStatus 枚举(Pending, Confirmed, Rejected, Modified) - 生成 EF Core 迁移 AddPendingConfig,创建 gw_pending_configs 表 ## 技术变更 ### NuGet 包 - 添加 KubernetesClient 16.0.2 包引用 ### 数据库 - 更新 ConsoleDbContext,添加 PendingConfigs DbSet - 配置 PendingConfig 表索引(Status, ServiceName, CreatedAt) ### EF Core 兼容性修复 - 修复 EF Core 10 中 JSON 值对象的映射问题 - 使用值转换器替代 OwnsOne().ToJson() 配置 ## 文件变更 - 新增: src/Services/K8sServiceWatchService.cs - 新增: src/Models/Entities/PendingConfig.cs - 新增: src/Models/Entities/PendingConfigEnums.cs - 新增: src/Migrations/20260307161935_AddPendingConfig.cs - 修改: src/Program.cs (注册 K8sServiceWatch) - 修改: src/Data/ConsoleDbContext.cs (添加实体配置) - 修改: Directory.Packages.props (添加 KubernetesClient) 关联任务: IMPL-1, IMPL-4 关联重构计划: WFS-gateway-refactor
This commit is contained in:
parent
154484d2dc
commit
3ec055b871
@ -23,6 +23,8 @@
|
||||
<PackageVersion Include="OpenIddict.Server.AspNetCore" Version="7.2.0" />
|
||||
<!-- Swashbuckle -->
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="10.1.4" />
|
||||
<!-- Kubernetes -->
|
||||
<PackageVersion Include="KubernetesClient" Version="16.0.2" />
|
||||
<!-- Graphics -->
|
||||
<PackageVersion Include="SkiaSharp" Version="3.119.2" />
|
||||
<PackageVersion Include="QRCoder" Version="1.7.0" />
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
using Fengling.Console.Models.Entities;
|
||||
using Fengling.Platform.Domain.AggregatesModel.GatewayAggregate;
|
||||
using Fengling.Platform.Domain.AggregatesModel.RoleAggregate;
|
||||
using Fengling.Platform.Domain.AggregatesModel.TenantAggregate;
|
||||
@ -6,6 +7,7 @@ using Fengling.Platform.Infrastructure;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Fengling.Console.Data;
|
||||
|
||||
@ -19,16 +21,46 @@ public class ConsoleDbContext : PlatformDbContext
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 待确认配置表
|
||||
/// </summary>
|
||||
public DbSet<PendingConfig> PendingConfigs { get; set; } = null!;
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
// ========== Gateway 模块 ==========
|
||||
modelBuilder.Entity<GwTenantRoute>(entity =>
|
||||
// ========== PendingConfig 模块 ==========
|
||||
modelBuilder.Entity<PendingConfig>(entity =>
|
||||
{
|
||||
entity.ToTable("gw_tenant_routes");
|
||||
entity.ToTable("gw_pending_configs");
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.Property(e => e.Id).HasMaxLength(32);
|
||||
entity.Property(e => e.SourceId).HasMaxLength(128);
|
||||
entity.Property(e => e.ServiceName).HasMaxLength(256);
|
||||
entity.Property(e => e.TenantCode).HasMaxLength(64);
|
||||
entity.Property(e => e.ClusterId).HasMaxLength(64);
|
||||
entity.Property(e => e.ConfirmedBy).HasMaxLength(128);
|
||||
entity.Property(e => e.ConfigJson).HasColumnType("text");
|
||||
|
||||
// 创建索引
|
||||
entity.HasIndex(e => e.Status);
|
||||
entity.HasIndex(e => e.Source);
|
||||
entity.HasIndex(e => e.Type);
|
||||
entity.HasIndex(e => new { e.TenantCode, e.Status });
|
||||
entity.HasIndex(e => new { e.ServiceName, e.Status });
|
||||
});
|
||||
|
||||
// ========== Gateway 模块 ==========
|
||||
// 忽略这些类型,让它们只作为 JSON 值对象使用(与 PlatformDbContext 保持一致)
|
||||
modelBuilder.Ignore<GwRouteMatch>();
|
||||
modelBuilder.Ignore<GwRouteHeader>();
|
||||
modelBuilder.Ignore<GwRouteQueryParameter>();
|
||||
modelBuilder.Ignore<GwTransform>();
|
||||
|
||||
// GwTenantRoute 已在 PlatformDbContext 中配置,这里只修改表名
|
||||
modelBuilder.Entity<GwTenantRoute>().ToTable("gw_tenant_routes");
|
||||
|
||||
// ========== Tenant 模块 ==========
|
||||
modelBuilder.Entity<Tenant>(entity =>
|
||||
{
|
||||
|
||||
@ -29,6 +29,7 @@
|
||||
<PackageReference Include="OpenIddict.EntityFrameworkCore" />
|
||||
<PackageReference Include="OpenIddict.Server" />
|
||||
<PackageReference Include="OpenIddict.Server.AspNetCore" />
|
||||
<PackageReference Include="KubernetesClient" />
|
||||
<PackageReference Include="SkiaSharp" />
|
||||
<PackageReference Include="QRCoder" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" />
|
||||
|
||||
868
src/Migrations/20260307161935_AddPendingConfig.Designer.cs
generated
Normal file
868
src/Migrations/20260307161935_AddPendingConfig.Designer.cs
generated
Normal file
@ -0,0 +1,868 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Fengling.Console.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Fengling.Console.Migrations
|
||||
{
|
||||
[DbContext(typeof(ConsoleDbContext))]
|
||||
[Migration("20260307161935_AddPendingConfig")]
|
||||
partial class AddPendingConfig
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.3")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Fengling.Console.Models.Entities.PendingConfig", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<string>("ClusterId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("ConfigJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime?>("ConfirmedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("ConfirmedBy")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("IsModified")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsNew")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("ServiceName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<int>("Source")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("SourceId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("TenantCode")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Source");
|
||||
|
||||
b.HasIndex("Status");
|
||||
|
||||
b.HasIndex("Type");
|
||||
|
||||
b.HasIndex("ServiceName", "Status");
|
||||
|
||||
b.HasIndex("TenantCode", "Status");
|
||||
|
||||
b.ToTable("gw_pending_configs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Fengling.Platform.Domain.AggregatesModel.GatewayAggregate.GwCluster", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ClusterId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<long?>("CreatedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime>("CreatedTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("LoadBalancingPolicy")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<long?>("UpdatedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime?>("UpdatedTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("Version")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ClusterId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("Name");
|
||||
|
||||
b.HasIndex("Status");
|
||||
|
||||
b.ToTable("GwClusters");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Fengling.Platform.Domain.AggregatesModel.GatewayAggregate.GwTenantRoute", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("AuthorizationPolicy")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("ClusterId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("CorsPolicy")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<long?>("CreatedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime>("CreatedTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsGlobal")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("LoadBalancingPolicy")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<int>("Priority")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("RateLimiterPolicy")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("ServiceName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("TenantCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<int?>("TimeoutSeconds")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<long?>("UpdatedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime?>("UpdatedTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("Version")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ClusterId");
|
||||
|
||||
b.HasIndex("ServiceName");
|
||||
|
||||
b.HasIndex("TenantCode");
|
||||
|
||||
b.HasIndex("ServiceName", "IsGlobal", "Status");
|
||||
|
||||
b.ToTable("gw_tenant_routes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Fengling.Platform.Domain.AggregatesModel.RoleAggregate.ApplicationRole", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("IsSystem")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.PrimitiveCollection<List<string>>("Permissions")
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.Property<long?>("TenantId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("RoleNameIndex");
|
||||
|
||||
b.ToTable("idn_roles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Fengling.Platform.Domain.AggregatesModel.TenantAggregate.Tenant", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<string>("ContactEmail")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("ContactName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("ContactPhone")
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<DateTime?>("ExpiresAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<int?>("MaxUsers")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<long>("RowVersion")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("TenantCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Status");
|
||||
|
||||
b.HasIndex("TenantCode")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("sys_tenants", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Fengling.Platform.Domain.AggregatesModel.UserAggregate.AccessLog", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<string>("Action")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("Duration")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("ErrorMessage")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("IpAddress")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Method")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)");
|
||||
|
||||
b.Property<string>("RequestData")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Resource")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<string>("ResponseData")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<string>("TenantId")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("UserAgent")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Action");
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.HasIndex("Status");
|
||||
|
||||
b.HasIndex("TenantId");
|
||||
|
||||
b.HasIndex("UserName");
|
||||
|
||||
b.ToTable("sys_access_logs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Fengling.Platform.Domain.AggregatesModel.UserAggregate.ApplicationUser", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("RealName")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTimeOffset?>("UpdatedTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedEmail")
|
||||
.HasDatabaseName("EmailIndex");
|
||||
|
||||
b.HasIndex("NormalizedUserName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UserNameIndex");
|
||||
|
||||
b.HasIndex("PhoneNumber")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("idn_users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Fengling.Platform.Domain.AggregatesModel.UserAggregate.AuditLog", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<string>("Action")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<string>("ErrorMessage")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("IpAddress")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("NewValue")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("OldValue")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Operation")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<string>("Operator")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<long?>("TargetId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("TargetName")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("TargetType")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("TenantId")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Action");
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.HasIndex("Operation");
|
||||
|
||||
b.HasIndex("Operator");
|
||||
|
||||
b.HasIndex("TenantId");
|
||||
|
||||
b.ToTable("sys_audit_logs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<long>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<long>("RoleId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("idn_role_claims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<long>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("idn_user_claims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<long>", b =>
|
||||
{
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ProviderKey")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ProviderDisplayName")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("LoginProvider", "ProviderKey");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("idn_user_logins", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<long>", b =>
|
||||
{
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("RoleId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("idn_user_roles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<long>", b =>
|
||||
{
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name");
|
||||
|
||||
b.ToTable("idn_user_tokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Fengling.Platform.Domain.AggregatesModel.GatewayAggregate.GwCluster", b =>
|
||||
{
|
||||
b.OwnsMany("Fengling.Platform.Domain.AggregatesModel.GatewayAggregate.GwDestination", "Destinations", b1 =>
|
||||
{
|
||||
b1.Property<string>("ClusterId")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b1.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property<int>("Id"));
|
||||
|
||||
b1.Property<string>("Address")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b1.Property<string>("DestinationId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b1.Property<string>("Health")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b1.Property<int>("HealthStatus")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.Property<int>("Weight")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.HasKey("ClusterId", "Id");
|
||||
|
||||
b1.HasIndex("ClusterId", "DestinationId");
|
||||
|
||||
b1.ToTable("GwDestination");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("ClusterId");
|
||||
});
|
||||
|
||||
b.OwnsOne("Fengling.Platform.Domain.AggregatesModel.GatewayAggregate.GwHealthCheckConfig", "HealthCheck", b1 =>
|
||||
{
|
||||
b1.Property<string>("GwClusterId");
|
||||
|
||||
b1.Property<bool>("Enabled");
|
||||
|
||||
b1.Property<int>("IntervalSeconds");
|
||||
|
||||
b1.Property<string>("Path");
|
||||
|
||||
b1.Property<int>("TimeoutSeconds");
|
||||
|
||||
b1.HasKey("GwClusterId");
|
||||
|
||||
b1.ToTable("GwClusters");
|
||||
|
||||
b1.ToJson("HealthCheck");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("GwClusterId");
|
||||
});
|
||||
|
||||
b.OwnsOne("Fengling.Platform.Domain.AggregatesModel.GatewayAggregate.GwSessionAffinityConfig", "SessionAffinity", b1 =>
|
||||
{
|
||||
b1.Property<string>("GwClusterId");
|
||||
|
||||
b1.Property<string>("AffinityKeyName")
|
||||
.IsRequired();
|
||||
|
||||
b1.Property<bool>("Enabled");
|
||||
|
||||
b1.Property<string>("Policy")
|
||||
.IsRequired();
|
||||
|
||||
b1.HasKey("GwClusterId");
|
||||
|
||||
b1.ToTable("GwClusters");
|
||||
|
||||
b1.ToJson("SessionAffinity");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("GwClusterId");
|
||||
});
|
||||
|
||||
b.Navigation("Destinations");
|
||||
|
||||
b.Navigation("HealthCheck");
|
||||
|
||||
b.Navigation("SessionAffinity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Fengling.Platform.Domain.AggregatesModel.UserAggregate.ApplicationUser", b =>
|
||||
{
|
||||
b.OwnsOne("Fengling.Platform.Domain.AggregatesModel.TenantAggregate.TenantInfo", "TenantInfo", b1 =>
|
||||
{
|
||||
b1.Property<long>("ApplicationUserId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<string>("TenantCode")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("TenantCode");
|
||||
|
||||
b1.Property<long?>("TenantId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("TenantId");
|
||||
|
||||
b1.Property<string>("TenantName")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("TenantName");
|
||||
|
||||
b1.HasKey("ApplicationUserId");
|
||||
|
||||
b1.ToTable("idn_users");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("ApplicationUserId");
|
||||
});
|
||||
|
||||
b.Navigation("TenantInfo");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<long>", b =>
|
||||
{
|
||||
b.HasOne("Fengling.Platform.Domain.AggregatesModel.RoleAggregate.ApplicationRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<long>", b =>
|
||||
{
|
||||
b.HasOne("Fengling.Platform.Domain.AggregatesModel.UserAggregate.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<long>", b =>
|
||||
{
|
||||
b.HasOne("Fengling.Platform.Domain.AggregatesModel.UserAggregate.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<long>", b =>
|
||||
{
|
||||
b.HasOne("Fengling.Platform.Domain.AggregatesModel.RoleAggregate.ApplicationRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Fengling.Platform.Domain.AggregatesModel.UserAggregate.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<long>", b =>
|
||||
{
|
||||
b.HasOne("Fengling.Platform.Domain.AggregatesModel.UserAggregate.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
281
src/Migrations/20260307161935_AddPendingConfig.cs
Normal file
281
src/Migrations/20260307161935_AddPendingConfig.cs
Normal file
@ -0,0 +1,281 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Fengling.Console.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddPendingConfig : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "gw_service_instances");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "gw_tenants");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "PathPattern",
|
||||
table: "gw_tenant_routes");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "AuthorizationPolicy",
|
||||
table: "gw_tenant_routes",
|
||||
type: "character varying(100)",
|
||||
maxLength: 100,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "CorsPolicy",
|
||||
table: "gw_tenant_routes",
|
||||
type: "character varying(100)",
|
||||
maxLength: 100,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "LoadBalancingPolicy",
|
||||
table: "gw_tenant_routes",
|
||||
type: "character varying(50)",
|
||||
maxLength: 50,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "RateLimiterPolicy",
|
||||
table: "gw_tenant_routes",
|
||||
type: "character varying(100)",
|
||||
maxLength: 100,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "TimeoutSeconds",
|
||||
table: "gw_tenant_routes",
|
||||
type: "integer",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "gw_pending_configs",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
Type = table.Column<int>(type: "integer", nullable: false),
|
||||
Source = table.Column<int>(type: "integer", nullable: false),
|
||||
SourceId = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||
ConfigJson = table.Column<string>(type: "text", nullable: false),
|
||||
ServiceName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
TenantCode = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
|
||||
ClusterId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
Status = table.Column<int>(type: "integer", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
ConfirmedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||
ConfirmedBy = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
|
||||
IsNew = table.Column<bool>(type: "boolean", nullable: false),
|
||||
IsModified = table.Column<bool>(type: "boolean", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_gw_pending_configs", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "GwClusters",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "text", nullable: false),
|
||||
ClusterId = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
Name = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
Description = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true),
|
||||
LoadBalancingPolicy = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||
Status = table.Column<int>(type: "integer", nullable: false),
|
||||
CreatedBy = table.Column<long>(type: "bigint", nullable: true),
|
||||
CreatedTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
UpdatedBy = table.Column<long>(type: "bigint", nullable: true),
|
||||
UpdatedTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||
IsDeleted = table.Column<bool>(type: "boolean", nullable: false),
|
||||
Version = table.Column<int>(type: "integer", nullable: false),
|
||||
HealthCheck = table.Column<string>(type: "jsonb", nullable: true),
|
||||
SessionAffinity = table.Column<string>(type: "jsonb", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_GwClusters", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "GwDestination",
|
||||
columns: table => new
|
||||
{
|
||||
ClusterId = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
DestinationId = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
Address = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||
Health = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
|
||||
Weight = table.Column<int>(type: "integer", nullable: false),
|
||||
HealthStatus = table.Column<int>(type: "integer", nullable: false),
|
||||
Status = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_GwDestination", x => new { x.ClusterId, x.Id });
|
||||
table.ForeignKey(
|
||||
name: "FK_GwDestination_GwClusters_ClusterId",
|
||||
column: x => x.ClusterId,
|
||||
principalTable: "GwClusters",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_gw_pending_configs_ServiceName_Status",
|
||||
table: "gw_pending_configs",
|
||||
columns: new[] { "ServiceName", "Status" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_gw_pending_configs_Source",
|
||||
table: "gw_pending_configs",
|
||||
column: "Source");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_gw_pending_configs_Status",
|
||||
table: "gw_pending_configs",
|
||||
column: "Status");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_gw_pending_configs_TenantCode_Status",
|
||||
table: "gw_pending_configs",
|
||||
columns: new[] { "TenantCode", "Status" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_gw_pending_configs_Type",
|
||||
table: "gw_pending_configs",
|
||||
column: "Type");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_GwClusters_ClusterId",
|
||||
table: "GwClusters",
|
||||
column: "ClusterId",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_GwClusters_Name",
|
||||
table: "GwClusters",
|
||||
column: "Name");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_GwClusters_Status",
|
||||
table: "GwClusters",
|
||||
column: "Status");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_GwDestination_ClusterId_DestinationId",
|
||||
table: "GwDestination",
|
||||
columns: new[] { "ClusterId", "DestinationId" });
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "gw_pending_configs");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "GwDestination");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "GwClusters");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AuthorizationPolicy",
|
||||
table: "gw_tenant_routes");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CorsPolicy",
|
||||
table: "gw_tenant_routes");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LoadBalancingPolicy",
|
||||
table: "gw_tenant_routes");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "RateLimiterPolicy",
|
||||
table: "gw_tenant_routes");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "TimeoutSeconds",
|
||||
table: "gw_tenant_routes");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "PathPattern",
|
||||
table: "gw_tenant_routes",
|
||||
type: "character varying(200)",
|
||||
maxLength: 200,
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "gw_service_instances",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "text", nullable: false),
|
||||
Address = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||
ClusterId = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
CreatedBy = table.Column<long>(type: "bigint", nullable: true),
|
||||
CreatedTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
DestinationId = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
Health = table.Column<int>(type: "integer", nullable: false),
|
||||
IsDeleted = table.Column<bool>(type: "boolean", nullable: false),
|
||||
Status = table.Column<int>(type: "integer", nullable: false),
|
||||
UpdatedBy = table.Column<long>(type: "bigint", nullable: true),
|
||||
UpdatedTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||
Version = table.Column<int>(type: "integer", nullable: false),
|
||||
Weight = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_gw_service_instances", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "gw_tenants",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
CreatedBy = table.Column<long>(type: "bigint", nullable: true),
|
||||
CreatedTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
IsDeleted = table.Column<bool>(type: "boolean", nullable: false),
|
||||
Status = table.Column<int>(type: "integer", nullable: false),
|
||||
TenantCode = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||
TenantName = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
UpdatedBy = table.Column<long>(type: "bigint", nullable: true),
|
||||
UpdatedTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||
Version = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_gw_tenants", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_gw_service_instances_ClusterId_DestinationId",
|
||||
table: "gw_service_instances",
|
||||
columns: new[] { "ClusterId", "DestinationId" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_gw_service_instances_Health",
|
||||
table: "gw_service_instances",
|
||||
column: "Health");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_gw_tenants_TenantCode",
|
||||
table: "gw_tenants",
|
||||
column: "TenantCode",
|
||||
unique: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -23,16 +23,80 @@ namespace Fengling.Console.Migrations
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Fengling.Platform.Domain.AggregatesModel.GatewayAggregate.GwServiceInstance", b =>
|
||||
modelBuilder.Entity("Fengling.Console.Models.Entities.PendingConfig", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<string>("ClusterId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("ConfigJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime?>("ConfirmedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("ConfirmedBy")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("IsModified")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsNew")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("ServiceName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<int>("Source")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("SourceId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("TenantCode")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Source");
|
||||
|
||||
b.HasIndex("Status");
|
||||
|
||||
b.HasIndex("Type");
|
||||
|
||||
b.HasIndex("ServiceName", "Status");
|
||||
|
||||
b.HasIndex("TenantCode", "Status");
|
||||
|
||||
b.ToTable("gw_pending_configs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Fengling.Platform.Domain.AggregatesModel.GatewayAggregate.GwCluster", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Address")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<string>("ClusterId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
@ -44,72 +108,26 @@ namespace Fengling.Console.Migrations
|
||||
b.Property<DateTime>("CreatedTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("DestinationId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<int>("Health")
|
||||
.HasColumnType("integer");
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<long?>("UpdatedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime?>("UpdatedTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("Version")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Weight")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Health");
|
||||
|
||||
b.HasIndex("ClusterId", "DestinationId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("gw_service_instances", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Fengling.Platform.Domain.AggregatesModel.GatewayAggregate.GwTenant", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<long?>("CreatedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime>("CreatedTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("TenantCode")
|
||||
b.Property<string>("LoadBalancingPolicy")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("TenantName")
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<long?>("UpdatedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
@ -121,10 +139,14 @@ namespace Fengling.Console.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TenantCode")
|
||||
b.HasIndex("ClusterId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("gw_tenants", (string)null);
|
||||
b.HasIndex("Name");
|
||||
|
||||
b.HasIndex("Status");
|
||||
|
||||
b.ToTable("GwClusters");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Fengling.Platform.Domain.AggregatesModel.GatewayAggregate.GwTenantRoute", b =>
|
||||
@ -132,11 +154,19 @@ namespace Fengling.Console.Migrations
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("AuthorizationPolicy")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("ClusterId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("CorsPolicy")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<long?>("CreatedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
@ -149,14 +179,17 @@ namespace Fengling.Console.Migrations
|
||||
b.Property<bool>("IsGlobal")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("PathPattern")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
b.Property<string>("LoadBalancingPolicy")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<int>("Priority")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("RateLimiterPolicy")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("ServiceName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
@ -170,6 +203,9 @@ namespace Fengling.Console.Migrations
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<int?>("TimeoutSeconds")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<long?>("UpdatedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
@ -645,6 +681,104 @@ namespace Fengling.Console.Migrations
|
||||
b.ToTable("idn_user_tokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Fengling.Platform.Domain.AggregatesModel.GatewayAggregate.GwCluster", b =>
|
||||
{
|
||||
b.OwnsMany("Fengling.Platform.Domain.AggregatesModel.GatewayAggregate.GwDestination", "Destinations", b1 =>
|
||||
{
|
||||
b1.Property<string>("ClusterId")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b1.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property<int>("Id"));
|
||||
|
||||
b1.Property<string>("Address")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b1.Property<string>("DestinationId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b1.Property<string>("Health")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b1.Property<int>("HealthStatus")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.Property<int>("Weight")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.HasKey("ClusterId", "Id");
|
||||
|
||||
b1.HasIndex("ClusterId", "DestinationId");
|
||||
|
||||
b1.ToTable("GwDestination");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("ClusterId");
|
||||
});
|
||||
|
||||
b.OwnsOne("Fengling.Platform.Domain.AggregatesModel.GatewayAggregate.GwHealthCheckConfig", "HealthCheck", b1 =>
|
||||
{
|
||||
b1.Property<string>("GwClusterId");
|
||||
|
||||
b1.Property<bool>("Enabled");
|
||||
|
||||
b1.Property<int>("IntervalSeconds");
|
||||
|
||||
b1.Property<string>("Path");
|
||||
|
||||
b1.Property<int>("TimeoutSeconds");
|
||||
|
||||
b1.HasKey("GwClusterId");
|
||||
|
||||
b1.ToTable("GwClusters");
|
||||
|
||||
b1.ToJson("HealthCheck");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("GwClusterId");
|
||||
});
|
||||
|
||||
b.OwnsOne("Fengling.Platform.Domain.AggregatesModel.GatewayAggregate.GwSessionAffinityConfig", "SessionAffinity", b1 =>
|
||||
{
|
||||
b1.Property<string>("GwClusterId");
|
||||
|
||||
b1.Property<string>("AffinityKeyName")
|
||||
.IsRequired();
|
||||
|
||||
b1.Property<bool>("Enabled");
|
||||
|
||||
b1.Property<string>("Policy")
|
||||
.IsRequired();
|
||||
|
||||
b1.HasKey("GwClusterId");
|
||||
|
||||
b1.ToTable("GwClusters");
|
||||
|
||||
b1.ToJson("SessionAffinity");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("GwClusterId");
|
||||
});
|
||||
|
||||
b.Navigation("Destinations");
|
||||
|
||||
b.Navigation("HealthCheck");
|
||||
|
||||
b.Navigation("SessionAffinity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Fengling.Platform.Domain.AggregatesModel.UserAggregate.ApplicationUser", b =>
|
||||
{
|
||||
b.OwnsOne("Fengling.Platform.Domain.AggregatesModel.TenantAggregate.TenantInfo", "TenantInfo", b1 =>
|
||||
|
||||
78
src/Models/Entities/PendingConfig.cs
Normal file
78
src/Models/Entities/PendingConfig.cs
Normal file
@ -0,0 +1,78 @@
|
||||
namespace Fengling.Console.Models.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// 待确认配置实体
|
||||
/// 用于存储从 K8s 服务发现或手动添加的待确认网关配置
|
||||
/// </summary>
|
||||
public class PendingConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// 唯一标识
|
||||
/// </summary>
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString("N");
|
||||
|
||||
/// <summary>
|
||||
/// 配置类型(Route, Cluster, Destination)
|
||||
/// </summary>
|
||||
public PendingConfigType Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 配置来源(K8sDiscovery, Manual)
|
||||
/// </summary>
|
||||
public PendingConfigSource Source { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 来源标识(K8s Service UID 或手动标识)
|
||||
/// </summary>
|
||||
public string SourceId { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// 配置内容(JSON 格式)
|
||||
/// </summary>
|
||||
public string ConfigJson { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// 服务名称
|
||||
/// </summary>
|
||||
public string ServiceName { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// 租户编码
|
||||
/// </summary>
|
||||
public string? TenantCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 集群ID
|
||||
/// </summary>
|
||||
public string ClusterId { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// 配置状态
|
||||
/// </summary>
|
||||
public PendingConfigStatus Status { get; set; } = PendingConfigStatus.Pending;
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// 确认时间
|
||||
/// </summary>
|
||||
public DateTime? ConfirmedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 确认人
|
||||
/// </summary>
|
||||
public string? ConfirmedBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否为新配置
|
||||
/// </summary>
|
||||
public bool IsNew { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否已修改
|
||||
/// </summary>
|
||||
public bool IsModified { get; set; }
|
||||
}
|
||||
64
src/Models/Entities/PendingConfigEnums.cs
Normal file
64
src/Models/Entities/PendingConfigEnums.cs
Normal file
@ -0,0 +1,64 @@
|
||||
namespace Fengling.Console.Models.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// 待确认配置类型
|
||||
/// </summary>
|
||||
public enum PendingConfigType
|
||||
{
|
||||
/// <summary>
|
||||
/// 路由配置
|
||||
/// </summary>
|
||||
Route = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 集群配置
|
||||
/// </summary>
|
||||
Cluster = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 目标地址配置
|
||||
/// </summary>
|
||||
Destination = 2
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 待确认配置来源
|
||||
/// </summary>
|
||||
public enum PendingConfigSource
|
||||
{
|
||||
/// <summary>
|
||||
/// K8s 服务发现
|
||||
/// </summary>
|
||||
K8sDiscovery = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 手动创建
|
||||
/// </summary>
|
||||
Manual = 1
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 待确认配置状态
|
||||
/// </summary>
|
||||
public enum PendingConfigStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// 待确认
|
||||
/// </summary>
|
||||
Pending = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 已确认
|
||||
/// </summary>
|
||||
Confirmed = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 已拒绝
|
||||
/// </summary>
|
||||
Rejected = 2,
|
||||
|
||||
/// <summary>
|
||||
/// 已修改
|
||||
/// </summary>
|
||||
Modified = 3
|
||||
}
|
||||
@ -12,6 +12,7 @@ using Microsoft.IdentityModel.Tokens;
|
||||
using System.Text;
|
||||
using NetCorePal.Extensions.DependencyInjection;
|
||||
using OpenIddict.Validation.AspNetCore;
|
||||
using k8s;
|
||||
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
@ -28,16 +29,6 @@ builder.Services.AddDbContext<ConsoleDbContext>(options =>
|
||||
}
|
||||
options.EnableDetailedErrors();
|
||||
});
|
||||
builder.Services.AddDbContext<PlatformDbContext>(options =>
|
||||
{
|
||||
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"));
|
||||
if (builder.Environment.IsDevelopment())
|
||||
{
|
||||
options.EnableSensitiveDataLogging();
|
||||
}
|
||||
options.EnableDetailedErrors();
|
||||
});
|
||||
|
||||
|
||||
// Use Platform's identity
|
||||
builder.Services.AddIdentity<ApplicationUser, ApplicationRole>()
|
||||
@ -67,7 +58,6 @@ builder.Services.AddScoped<IH5LinkService, H5LinkService>();
|
||||
|
||||
builder.Services.AddOpenIddict()
|
||||
.AddCore(options => { options.UseEntityFrameworkCore().UseDbContext<ConsoleDbContext>(); })
|
||||
.AddCore(options => { options.UseEntityFrameworkCore().UseDbContext<PlatformDbContext>(); })
|
||||
.AddValidation(options =>
|
||||
{
|
||||
options.SetIssuer("http://localhost:5132/");
|
||||
@ -105,7 +95,9 @@ builder.Services.AddSwaggerGen(c =>
|
||||
c.CustomSchemaIds(type => type.FullName);
|
||||
});
|
||||
|
||||
builder.Services.AddRepositories(typeof(ConsoleDbContext).Assembly);
|
||||
|
||||
// 添加 K8s 服务监视
|
||||
builder.Services.AddK8sServiceWatch();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
|
||||
427
src/Services/K8sServiceWatchService.cs
Normal file
427
src/Services/K8sServiceWatchService.cs
Normal file
@ -0,0 +1,427 @@
|
||||
using System.Threading.Channels;
|
||||
using k8s;
|
||||
using k8s.Models;
|
||||
|
||||
namespace Fengling.Console.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Kubernetes 服务变更事件
|
||||
/// </summary>
|
||||
public sealed record K8sServiceEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// 事件类型:Added, Modified, Deleted, Error, Bookmark
|
||||
/// </summary>
|
||||
public required WatchEventType EventType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 服务对象
|
||||
/// </summary>
|
||||
public required V1Service Service { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 事件发生时间
|
||||
/// </summary>
|
||||
public DateTime Timestamp { get; init; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Kubernetes 服务监视服务
|
||||
/// 监视 K8s Service 资源变化并发布通知
|
||||
/// </summary>
|
||||
public sealed class K8sServiceWatchService : BackgroundService, IDisposable
|
||||
{
|
||||
private readonly ILogger<K8sServiceWatchService> _logger;
|
||||
private readonly IKubernetes _kubernetesClient;
|
||||
private readonly INotificationService _notificationService;
|
||||
private readonly K8sServiceWatchOptions _options;
|
||||
|
||||
// 用于通知新事件的通道
|
||||
private readonly Channel<K8sServiceEvent> _eventChannel;
|
||||
|
||||
// 取消令牌源,用于控制重连循环
|
||||
private CancellationTokenSource? _watchCts;
|
||||
|
||||
// 当前退避延迟
|
||||
private TimeSpan _currentBackoff;
|
||||
|
||||
// 是否已释放
|
||||
private bool _disposed;
|
||||
|
||||
public K8sServiceWatchService(
|
||||
ILogger<K8sServiceWatchService> logger,
|
||||
IKubernetes kubernetesClient,
|
||||
INotificationService notificationService,
|
||||
IConfiguration configuration)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_kubernetesClient = kubernetesClient ?? throw new ArgumentNullException(nameof(kubernetesClient));
|
||||
_notificationService = notificationService ?? throw new ArgumentNullException(nameof(notificationService));
|
||||
|
||||
_options = new K8sServiceWatchOptions();
|
||||
configuration.GetSection("K8sServiceWatch").Bind(_options);
|
||||
|
||||
// 创建有界通道,防止内存无限增长
|
||||
var channelOptions = new BoundedChannelOptions(_options.ChannelCapacity)
|
||||
{
|
||||
FullMode = BoundedChannelFullMode.DropOldest
|
||||
};
|
||||
_eventChannel = Channel.CreateBounded<K8sServiceEvent>(channelOptions);
|
||||
|
||||
_currentBackoff = _options.InitialBackoff;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 主执行循环
|
||||
/// </summary>
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
_logger.LogInformation("K8sServiceWatchService 启动,开始监视 Service 资源...");
|
||||
|
||||
// 启动事件处理任务
|
||||
var processTask = ProcessEventsAsync(stoppingToken);
|
||||
|
||||
try
|
||||
{
|
||||
// 主重连循环
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await RunWatchLoopAsync(stoppingToken);
|
||||
}
|
||||
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
_logger.LogInformation("Watch 循环因服务停止而取消");
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Watch 循环发生错误,将在 {BackoffMs}ms 后重试", _currentBackoff.TotalMilliseconds);
|
||||
await WaitWithBackoffAsync(stoppingToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 确保事件处理任务也停止
|
||||
_eventChannel.Writer.TryComplete();
|
||||
try
|
||||
{
|
||||
await processTask.WaitAsync(TimeSpan.FromSeconds(5), CancellationToken.None);
|
||||
}
|
||||
catch (TimeoutException)
|
||||
{
|
||||
_logger.LogWarning("事件处理任务未在 5 秒内完成");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "等待事件处理任务时发生错误");
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("K8sServiceWatchService 已停止");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 运行单次 Watch 循环
|
||||
/// </summary>
|
||||
private async Task RunWatchLoopAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
// 创建本次 Watch 的取消令牌源
|
||||
_watchCts = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken);
|
||||
var watchToken = _watchCts.Token;
|
||||
|
||||
_logger.LogInformation("开始 Watch Kubernetes Services (命名空间: {Namespace})",
|
||||
string.IsNullOrEmpty(_options.Namespace) ? "全部" : _options.Namespace);
|
||||
|
||||
try
|
||||
{
|
||||
// 构建标签选择器
|
||||
var labelSelector = _options.LabelSelector;
|
||||
|
||||
// 启动 Watch
|
||||
var responseTask = string.IsNullOrEmpty(_options.Namespace)
|
||||
? _kubernetesClient.CoreV1.ListServiceForAllNamespacesWithHttpMessagesAsync(
|
||||
watch: true,
|
||||
labelSelector: labelSelector,
|
||||
cancellationToken: watchToken)
|
||||
: _kubernetesClient.CoreV1.ListNamespacedServiceWithHttpMessagesAsync(
|
||||
namespaceParameter: _options.Namespace,
|
||||
watch: true,
|
||||
labelSelector: labelSelector,
|
||||
cancellationToken: watchToken);
|
||||
|
||||
var response = await responseTask;
|
||||
|
||||
// 使用扩展方法 Watch
|
||||
var watchable = response.Watch<V1Service, V1ServiceList>(
|
||||
onEvent: (type, service) =>
|
||||
{
|
||||
_ = HandleServiceEventAsync(type, service);
|
||||
},
|
||||
onError: (ex) =>
|
||||
{
|
||||
_logger.LogError(ex, "Watch 发生错误");
|
||||
},
|
||||
onClosed: () =>
|
||||
{
|
||||
_logger.LogWarning("Watch 连接已关闭");
|
||||
});
|
||||
|
||||
// 等待直到取消
|
||||
await Task.Delay(Timeout.Infinite, watchToken);
|
||||
}
|
||||
catch (OperationCanceledException) when (watchToken.IsCancellationRequested)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Watch 过程中发生异常");
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_watchCts?.Cancel();
|
||||
_watchCts?.Dispose();
|
||||
_watchCts = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理 Service 事件
|
||||
/// </summary>
|
||||
private async Task HandleServiceEventAsync(WatchEventType type, V1Service service)
|
||||
{
|
||||
try
|
||||
{
|
||||
var @event = new K8sServiceEvent
|
||||
{
|
||||
EventType = type,
|
||||
Service = service
|
||||
};
|
||||
|
||||
// 尝试写入通道,不阻塞
|
||||
await _eventChannel.Writer.WriteAsync(@event);
|
||||
|
||||
_logger.LogDebug("Service 事件已入队: {Name}/{Namespace}, 类型: {Type}",
|
||||
service.Metadata?.Name,
|
||||
service.Metadata?.NamespaceProperty,
|
||||
type);
|
||||
}
|
||||
catch (ChannelClosedException)
|
||||
{
|
||||
_logger.LogWarning("事件通道已关闭,无法写入事件");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "处理 Service 事件时发生错误");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理事件队列
|
||||
/// </summary>
|
||||
private async Task ProcessEventsAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
await foreach (var @event in _eventChannel.Reader.ReadAllAsync(stoppingToken))
|
||||
{
|
||||
try
|
||||
{
|
||||
await ProcessSingleEventAsync(@event, stoppingToken);
|
||||
|
||||
// 成功处理后重置退避
|
||||
ResetBackoff();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "处理单个事件时发生错误");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理单个事件
|
||||
/// </summary>
|
||||
private async Task ProcessSingleEventAsync(K8sServiceEvent @event, CancellationToken cancellationToken)
|
||||
{
|
||||
var service = @event.Service;
|
||||
var metadata = service.Metadata;
|
||||
|
||||
if (metadata == null)
|
||||
{
|
||||
_logger.LogWarning("收到无效的 Service 事件:Metadata 为空");
|
||||
return;
|
||||
}
|
||||
|
||||
var eventAction = @event.EventType switch
|
||||
{
|
||||
WatchEventType.Added => "create",
|
||||
WatchEventType.Modified => "update",
|
||||
WatchEventType.Deleted => "delete",
|
||||
_ => "unknown"
|
||||
};
|
||||
|
||||
_logger.LogInformation("Service {Action}: {Name}/{Namespace}",
|
||||
eventAction,
|
||||
metadata.Name,
|
||||
metadata.NamespaceProperty);
|
||||
|
||||
// 发布配置变更通知
|
||||
var details = new
|
||||
{
|
||||
ServiceName = metadata.Name,
|
||||
Namespace = metadata.NamespaceProperty,
|
||||
ClusterIP = service.Spec?.ClusterIP,
|
||||
ExternalIPs = service.Spec?.ExternalIPs,
|
||||
Ports = service.Spec?.Ports?.Select(p => new { p.Name, p.Port, p.TargetPort, p.Protocol }),
|
||||
Labels = metadata.Labels,
|
||||
EventTimestamp = @event.Timestamp
|
||||
};
|
||||
|
||||
await _notificationService.PublishConfigChangeAsync(
|
||||
"service",
|
||||
eventAction,
|
||||
details,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 带指数退避的等待
|
||||
/// </summary>
|
||||
private async Task WaitWithBackoffAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(_currentBackoff, cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// 正常取消,不做处理
|
||||
}
|
||||
|
||||
// 增加退避时间
|
||||
_currentBackoff = TimeSpan.FromMilliseconds(
|
||||
Math.Min(
|
||||
_currentBackoff.TotalMilliseconds * _options.BackoffMultiplier,
|
||||
_options.MaxBackoff.TotalMilliseconds));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重置退避时间
|
||||
/// </summary>
|
||||
private void ResetBackoff()
|
||||
{
|
||||
_currentBackoff = _options.InitialBackoff;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停止服务
|
||||
/// </summary>
|
||||
public override async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("正在停止 K8sServiceWatchService...");
|
||||
|
||||
// 取消当前的 Watch
|
||||
_watchCts?.Cancel();
|
||||
|
||||
// 关闭事件通道
|
||||
_eventChannel.Writer.TryComplete();
|
||||
|
||||
await base.StopAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放资源
|
||||
/// </summary>
|
||||
public override void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_watchCts?.Cancel();
|
||||
_watchCts?.Dispose();
|
||||
_eventChannel.Writer.TryComplete();
|
||||
|
||||
_disposed = true;
|
||||
|
||||
base.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// K8s 服务监视配置选项
|
||||
/// </summary>
|
||||
public sealed class K8sServiceWatchOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 要监视的命名空间,空字符串表示监视所有命名空间
|
||||
/// </summary>
|
||||
public string Namespace { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// 标签选择器,用于过滤 Service
|
||||
/// </summary>
|
||||
public string? LabelSelector { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 初始退避时间
|
||||
/// </summary>
|
||||
public TimeSpan InitialBackoff { get; set; } = TimeSpan.FromSeconds(1);
|
||||
|
||||
/// <summary>
|
||||
/// 最大退避时间
|
||||
/// </summary>
|
||||
public TimeSpan MaxBackoff { get; set; } = TimeSpan.FromMinutes(5);
|
||||
|
||||
/// <summary>
|
||||
/// 退避乘数
|
||||
/// </summary>
|
||||
public double BackoffMultiplier { get; set; } = 2.0;
|
||||
|
||||
/// <summary>
|
||||
/// 事件通道容量
|
||||
/// </summary>
|
||||
public int ChannelCapacity { get; set; } = 1000;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 服务扩展
|
||||
/// </summary>
|
||||
public static class K8sServiceWatchServiceExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 添加 K8s 服务监视服务
|
||||
/// </summary>
|
||||
public static IServiceCollection AddK8sServiceWatch(this IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<IKubernetes>(provider =>
|
||||
{
|
||||
var logger = provider.GetRequiredService<ILogger<IKubernetes>>();
|
||||
|
||||
try
|
||||
{
|
||||
// 尝试加载集群内配置
|
||||
var config = KubernetesClientConfiguration.InClusterConfig();
|
||||
logger.LogInformation("使用集群内 Kubernetes 配置");
|
||||
return new Kubernetes(config);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "无法加载集群内配置,尝试加载本地 kubeconfig");
|
||||
|
||||
// 回退到本地 kubeconfig
|
||||
var config = KubernetesClientConfiguration.BuildConfigFromConfigFile();
|
||||
logger.LogInformation("使用本地 Kubernetes 配置");
|
||||
return new Kubernetes(config);
|
||||
}
|
||||
});
|
||||
|
||||
services.AddHostedService<K8sServiceWatchService>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user