Record project continuation changes

This commit is contained in:
2026-05-12 21:02:29 +03:00
parent 3059d1d7a3
commit 8f69d53193
339 changed files with 101111 additions and 1769 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
FROM golang:1.23-bookworm AS build
FROM golang:1.25-bookworm AS build
WORKDIR /src
+10 -9
View File
@@ -1,23 +1,24 @@
module github.com/example/remote-access-platform/backend
go 1.23.2
go 1.25.0
require (
github.com/go-chi/chi/v5 v5.2.1
github.com/golang-jwt/jwt/v5 v5.2.2
github.com/go-chi/chi/v5 v5.2.5
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
github.com/jackc/pgx/v5 v5.7.4
github.com/redis/go-redis/v9 v9.8.0
golang.org/x/crypto v0.37.0
github.com/jackc/pgx/v5 v5.9.2
github.com/redis/go-redis/v9 v9.19.0
golang.org/x/crypto v0.50.0
)
require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
golang.org/x/sync v0.13.0 // indirect
golang.org/x/text v0.24.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/text v0.36.0 // indirect
)
+24 -18
View File
@@ -7,12 +7,10 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
@@ -21,25 +19,33 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg=
github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI=
github.com/redis/go-redis/v9 v9.8.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
github.com/redis/go-redis/v9 v9.19.0 h1:XPVaaPSnG6RhYf7p+rmSa9zZfeVAnWsH5h3lxthOm/k=
github.com/redis/go-redis/v9 v9.19.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+15 -7
View File
@@ -14,12 +14,13 @@ const (
)
type User struct {
ID string
Email string
PasswordHash string
MFAEnabled bool
CreatedAt time.Time
UpdatedAt time.Time
ID string `json:"id"`
Email string `json:"email"`
PasswordHash string `json:"-"`
MFAEnabled bool `json:"mfa_enabled"`
PlatformRole string `json:"platform_role"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type Device struct {
@@ -40,7 +41,7 @@ type AuthSession struct {
ID string
UserID string
DeviceID string
RefreshTokenHash string
RefreshTokenHash string `json:"-"`
RefreshExpiresAt time.Time
LastSeenAt *time.Time
LastRotatedAt *time.Time
@@ -69,6 +70,13 @@ type BootstrapOwnerCommand struct {
ActivationSignature string `json:"activation_signature"`
}
type CreateUserCommand struct {
ActorUserID string `json:"actor_user_id"`
Email string `json:"email"`
Password string `json:"password"`
PlatformRole string `json:"platform_role"`
}
type RevokeAuthSessionCommand struct {
UserID string `json:"user_id"`
AuthSessionID string `json:"auth_session_id"`
+30
View File
@@ -34,6 +34,10 @@ func (m *Module) RegisterRoutes(router chi.Router) {
r.Get("/devices", m.handleTrustedDevices)
r.Post("/devices/{deviceID}/revoke", m.handleRevokeTrustedDevice)
})
router.Route("/users", func(r chi.Router) {
r.Get("/", m.handleListUsers)
r.Post("/", m.handleCreateUser)
})
}
func (m *Module) handleInstallationStatus(w http.ResponseWriter, r *http.Request) {
@@ -78,6 +82,32 @@ func (m *Module) handleLogin(w http.ResponseWriter, r *http.Request) {
httpx.WriteJSON(w, http.StatusOK, result)
}
func (m *Module) handleListUsers(w http.ResponseWriter, r *http.Request) {
actorUserID := r.URL.Query().Get("actor_user_id")
users, err := m.service.ListUsers(r.Context(), actorUserID)
if err != nil {
status, message := m.service.MapError(err)
httpx.WriteError(w, status, message)
return
}
httpx.WriteJSON(w, http.StatusOK, map[string]any{"users": users})
}
func (m *Module) handleCreateUser(w http.ResponseWriter, r *http.Request) {
var cmd CreateUserCommand
if err := json.NewDecoder(r.Body).Decode(&cmd); err != nil {
httpx.WriteError(w, http.StatusBadRequest, "invalid user payload")
return
}
user, err := m.service.CreateUser(r.Context(), cmd)
if err != nil {
status, message := m.service.MapError(err)
httpx.WriteError(w, status, message)
return
}
httpx.WriteJSON(w, http.StatusCreated, map[string]any{"user": user})
}
func (m *Module) handleRefresh(w http.ResponseWriter, r *http.Request) {
var cmd RefreshCommand
if err := json.NewDecoder(r.Body).Decode(&cmd); err != nil {
@@ -70,7 +70,7 @@ type postgresInstallationRepository struct {
func (r *postgresUserRepository) GetByEmail(ctx context.Context, email string) (*User, error) {
const query = `
SELECT id::text, email, password_hash, mfa_enabled, created_at, updated_at
SELECT id::text, email, password_hash, mfa_enabled, platform_role, created_at, updated_at
FROM users
WHERE email = $1
`
@@ -79,13 +79,53 @@ WHERE email = $1
func (r *postgresUserRepository) GetByID(ctx context.Context, userID string) (*User, error) {
const query = `
SELECT id::text, email, password_hash, mfa_enabled, created_at, updated_at
SELECT id::text, email, password_hash, mfa_enabled, platform_role, created_at, updated_at
FROM users
WHERE id = $1::uuid
`
return scanOptionalUser(r.db.QueryRow(ctx, query, userID))
}
func (r *postgresUserRepository) List(ctx context.Context) ([]User, error) {
const query = `
SELECT id::text, email, password_hash, mfa_enabled, platform_role, created_at, updated_at
FROM users
ORDER BY created_at DESC
`
rows, err := r.db.Query(ctx, query)
if err != nil {
return nil, fmt.Errorf("query users: %w", err)
}
defer rows.Close()
var users []User
for rows.Next() {
user, err := scanOptionalUser(rows)
if err != nil {
return nil, err
}
if user != nil {
users = append(users, *user)
}
}
return users, rows.Err()
}
func (r *postgresUserRepository) Create(ctx context.Context, user User) (*User, error) {
const query = `
INSERT INTO users (email, password_hash, mfa_enabled, platform_role, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id::text, email, password_hash, mfa_enabled, platform_role, created_at, updated_at
`
return scanOptionalUser(r.db.QueryRow(ctx, query,
user.Email,
user.PasswordHash,
user.MFAEnabled,
user.PlatformRole,
user.CreatedAt,
user.UpdatedAt,
))
}
func (r *postgresDeviceRepository) Upsert(ctx context.Context, params UpsertDeviceParams) (*Device, error) {
const query = `
INSERT INTO devices (
@@ -348,7 +388,7 @@ ON CONFLICT (email) DO UPDATE SET
password_hash = EXCLUDED.password_hash,
platform_role = EXCLUDED.platform_role,
updated_at = EXCLUDED.updated_at
RETURNING id::text, email, password_hash, mfa_enabled, created_at, updated_at
RETURNING id::text, email, password_hash, mfa_enabled, platform_role, created_at, updated_at
`, email, params.PasswordHash, params.Role, now))
if err != nil {
return nil, fmt.Errorf("upsert bootstrap owner: %w", err)
@@ -461,6 +501,7 @@ func scanOptionalUser(row scanner) (*User, error) {
&user.Email,
&user.PasswordHash,
&user.MFAEnabled,
&user.PlatformRole,
&user.CreatedAt,
&user.UpdatedAt,
); err != nil {
@@ -7,8 +7,10 @@ import (
)
type UserRepository interface {
List(ctx context.Context) ([]User, error)
GetByEmail(ctx context.Context, email string) (*User, error)
GetByID(ctx context.Context, userID string) (*User, error)
Create(ctx context.Context, user User) (*User, error)
}
type DeviceRepository interface {
+67 -1
View File
@@ -13,11 +13,13 @@ import (
"github.com/example/remote-access-platform/backend/internal/platform/authority"
"github.com/example/remote-access-platform/backend/internal/platform/module"
postgresplatform "github.com/example/remote-access-platform/backend/internal/platform/postgres"
)
type Service struct {
cfg module.Config
store Store
db postgresplatform.DBTX
transactor Transactor
tokenManager *TokenManager
authority *authority.Verifier
@@ -31,7 +33,7 @@ func NewService(deps module.Dependencies, store Store, transactor Transactor, ve
} else if verifier, err := authority.NewVerifier(deps.Config.Installation); err == nil {
authorityVerifier = verifier
}
return &Service{
service := &Service{
cfg: deps.Config,
store: store,
transactor: transactor,
@@ -45,6 +47,10 @@ func NewService(deps module.Dependencies, store Store, transactor Transactor, ve
authority: authorityVerifier,
now: time.Now,
}
if postgresStore, ok := store.(*postgresStore); ok {
service.db = postgresStore.db
}
return service
}
func (s *Service) Login(ctx context.Context, cmd LoginCommand) (*AuthResult, error) {
@@ -120,6 +126,44 @@ func (s *Service) Login(ctx context.Context, cmd LoginCommand) (*AuthResult, err
return &result, nil
}
func (s *Service) ListUsers(ctx context.Context, actorUserID string) ([]User, error) {
if err := s.ensurePlatformAdmin(ctx, actorUserID); err != nil {
return nil, err
}
return s.store.Users().List(ctx)
}
func (s *Service) CreateUser(ctx context.Context, cmd CreateUserCommand) (*User, error) {
if err := s.ensurePlatformAdmin(ctx, cmd.ActorUserID); err != nil {
return nil, err
}
email := strings.ToLower(strings.TrimSpace(cmd.Email))
password := strings.TrimSpace(cmd.Password)
role := strings.TrimSpace(cmd.PlatformRole)
if role == "" {
role = "user"
}
if email == "" || !strings.Contains(email, "@") || len(password) < 8 {
return nil, ErrInvalidBootstrapOwner
}
if role != "user" && role != authority.PlatformRoleAdmin && role != authority.PlatformRoleRecoveryAdmin {
return nil, ErrInvalidBootstrapOwner
}
passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return nil, fmt.Errorf("hash user password: %w", err)
}
now := s.now().UTC()
return s.store.Users().Create(ctx, User{
Email: email,
PasswordHash: string(passwordHash),
MFAEnabled: false,
PlatformRole: role,
CreatedAt: now,
UpdatedAt: now,
})
}
func (s *Service) Refresh(ctx context.Context, cmd RefreshCommand) (*AuthResult, error) {
authSessionID, err := s.tokenManager.ParseRefreshToken(cmd.RefreshToken)
if err != nil {
@@ -438,3 +482,25 @@ func (s *Service) installationStatusFromRecord(record *InstallationAuthorityStat
func (s *Service) strictAuthority() bool {
return s.authority != nil && s.authority.Strict()
}
func (s *Service) ensurePlatformAdmin(ctx context.Context, actorUserID string) error {
if actorUserID == "" {
return ErrInvalidCredentials
}
role := authority.PlatformRoleUser
if s.db != nil {
effectiveRole, err := authority.EffectivePlatformRole(ctx, s.db, s.authority, actorUserID)
if err != nil {
return err
}
role = effectiveRole
} else if user, err := s.store.Users().GetByID(ctx, actorUserID); err != nil {
return err
} else if user != nil && user.PlatformRole != "" {
role = user.PlatformRole
}
if role != authority.PlatformRoleAdmin && role != authority.PlatformRoleRecoveryAdmin {
return ErrDeviceRevoked
}
return nil
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,216 @@
package cluster
import (
"context"
"testing"
"time"
)
func TestVPNPacketHubPopBatchAndStatsKeys(t *testing.T) {
hub := newVPNPacketHub()
key := vpnPacketKey{
ClusterID: "cluster-1",
VPNConnectionID: "vpn-1",
Direction: vpnDirectionClientToGateway,
}
packetA := []byte{
0x45, 0x00, 0x00, 20,
0x00, 0x01, 0x00, 0x00,
64, 17, 0, 0,
192, 168, 0, 1,
192, 168, 0, 2,
0x00, 0x50, 0x01, 0xBB,
}
packetB := make([]byte, len(packetA))
copy(packetB, packetA)
packetB[19] = 0xBA
hub.Push(key, packetA)
hub.Push(key, packetB)
packets := hub.PopBatch(context.Background(), key, 0, vpnPacketBatchMaxPackets, vpnPacketBatchMaxBytes)
if len(packets) != 2 {
t.Fatalf("expected 2 packets in batch, got %d", len(packets))
}
statsAny := hub.Snapshot("cluster-1", "vpn-1")[vpnDirectionClientToGateway]
stats, ok := statsAny.(map[string]any)
if !ok {
t.Fatalf("unexpected stats payload type: %T", statsAny)
}
for _, keyName := range []string{
"pushed",
"pushed_bytes",
"popped",
"popped_bytes",
"window_push_rate_pps",
"window_pop_rate_pps",
"window_push_rate_mbps",
"window_pop_rate_mbps",
"window_push_packets",
"window_pop_packets",
"queue_depth",
"queue_depths",
"queue_depth_max",
"queue_depth_high_watermark",
"queue_depth_high_at",
"shard_depth_high_watermark",
"shard_depth_high_at",
"queue_capacity",
"queue_shard_capacity",
"queue_full_drops",
"requeue_drops",
"cleared_stale_packets",
"flow_shard_count",
"flow_isolation",
} {
if _, found := stats[keyName]; !found {
t.Fatalf("missing vpn packet stat key %s", keyName)
}
}
if got, ok := stats["popped"].(uint64); !ok || got != 2 {
t.Fatalf("expected popped=2, got %v (ok=%v)", stats["popped"], ok)
}
if got, ok := stats["pushed"].(uint64); !ok || got != 2 {
t.Fatalf("expected pushed=2, got %v (ok=%v)", stats["pushed"], ok)
}
if got, ok := stats["queue_depth_high_watermark"].(int); !ok || got < 1 {
t.Fatalf("expected queue depth high watermark, got %v (ok=%v)", stats["queue_depth_high_watermark"], ok)
}
}
func TestVPNPacketHubGatherBehavior(t *testing.T) {
hub := newVPNPacketHub()
key := vpnPacketKey{
ClusterID: "cluster-1",
VPNConnectionID: "vpn-1",
Direction: vpnDirectionGatewayToClient,
}
packet := []byte{
0x45, 0x00, 0x00, 20,
0x00, 0x01, 0x00, 0x00,
64, 6, 0, 0,
10, 0, 0, 1,
10, 0, 0, 2,
0x12, 0x34, 0x56, 0x78,
}
hub.Push(key, packet)
hub.Push(key, packet)
hub.Push(key, packet)
first, ok := hub.Pop(context.Background(), key, 0)
if !ok {
t.Fatal("expected packet from queue")
}
batch := hub.PopBatch(context.Background(), key, 0, 1, 1024)
if len(batch) != 1 {
t.Fatalf("expected 1 packet because batch limit 1, got %d", len(batch))
}
_ = first
}
func TestVPNPacketHubFlowShardsReportDepths(t *testing.T) {
hub := newVPNPacketHub()
key := vpnPacketKey{
ClusterID: "cluster-1",
VPNConnectionID: "vpn-1",
Direction: vpnDirectionGatewayToClient,
}
for i := byte(1); i <= 8; i++ {
packet := []byte{
0x45, 0x00, 0x00, 24,
0x00, i, 0x00, 0x00,
64, 6, 0, 0,
10, 0, 0, i,
192, 168, 200, i,
0x12, i, 0x56, i,
}
if err := hub.Push(key, packet); err != nil {
t.Fatalf("push packet %d: %v", i, err)
}
}
statsAny := hub.Snapshot("cluster-1", "vpn-1")[vpnDirectionGatewayToClient]
stats, ok := statsAny.(map[string]any)
if !ok {
t.Fatalf("unexpected stats payload type: %T", statsAny)
}
if got, ok := stats["queue_depth"].(int); !ok || got != 8 {
t.Fatalf("expected queue_depth=8, got %v (ok=%v)", stats["queue_depth"], ok)
}
depths, ok := stats["queue_depths"].([]int)
if !ok {
t.Fatalf("unexpected queue_depths payload type: %T", stats["queue_depths"])
}
if len(depths) != vpnPacketFlowShardCount {
t.Fatalf("expected %d queue shards, got %d", vpnPacketFlowShardCount, len(depths))
}
nonEmpty := 0
for _, depth := range depths {
if depth > 0 {
nonEmpty++
}
}
if nonEmpty < 2 {
t.Fatalf("expected packets to be distributed across at least 2 shards, got depths=%v", depths)
}
}
func TestVPNPacketHubClearDoesNotCountAsDrop(t *testing.T) {
hub := newVPNPacketHub()
key := vpnPacketKey{
ClusterID: "cluster-1",
VPNConnectionID: "vpn-1",
Direction: vpnDirectionClientToGateway,
}
packet := []byte{
0x45, 0x00, 0x00, 20,
0x00, 0x01, 0x00, 0x00,
64, 6, 0, 0,
10, 0, 0, 1,
10, 0, 0, 2,
0x12, 0x34, 0x56, 0x78,
}
if err := hub.Push(key, packet); err != nil {
t.Fatalf("push packet: %v", err)
}
if cleared := hub.Clear(key); cleared != 1 {
t.Fatalf("expected cleared=1, got %d", cleared)
}
statsAny := hub.Snapshot("cluster-1", "vpn-1")[vpnDirectionClientToGateway]
stats, ok := statsAny.(map[string]any)
if !ok {
t.Fatalf("unexpected stats payload type: %T", statsAny)
}
if got, ok := stats["dropped"].(uint64); !ok || got != 0 {
t.Fatalf("expected dropped=0 for stale clear, got %v (ok=%v)", stats["dropped"], ok)
}
if got, ok := stats["cleared_stale_packets"].(uint64); !ok || got != 1 {
t.Fatalf("expected cleared_stale_packets=1, got %v (ok=%v)", stats["cleared_stale_packets"], ok)
}
}
func TestVPNClientDiagnosticStopCommandDrainsPendingWork(t *testing.T) {
hub := newVPNClientDiagnosticHub()
hub.Enqueue("cluster-1", "device-1", map[string]any{"type": "vpn_page_probe", "url": "https://speedtest.rt.ru/"})
hub.Enqueue("cluster-1", "device-1", map[string]any{"type": "vpn_tcp_connect", "host": "192.168.200.95"})
hub.Enqueue("cluster-1", "device-1", map[string]any{"type": "stop_vpn"})
item, ok := hub.Pop(context.Background(), "cluster-1", "device-1", time.Millisecond)
if !ok {
t.Fatal("expected priority stop command")
}
if got, _ := item.Payload["type"].(string); got != "stop_vpn" {
t.Fatalf("first command = %q, want stop_vpn", got)
}
if item, ok := hub.Pop(context.Background(), "cluster-1", "device-1", 0); ok {
t.Fatalf("expected old commands to be drained, got %#v", item.Payload)
}
}
File diff suppressed because it is too large Load Diff
@@ -32,3 +32,136 @@ func TestMeshLatestObservationKeyDefaults(t *testing.T) {
t.Fatalf("key = %q", key)
}
}
func TestEnrichVPNClientFabricRoutePrefersPlacementEntryAndActiveExit(t *testing.T) {
item := VPNClientConnection{
AllowedNodeIDs: []string{"node-a", "node-b", "node-b"},
EntryNodeIDs: []string{"entry-1", "entry-2"},
ExitNodeID: "exit-policy",
ActiveLease: &NodeVPNAssignmentLease{
OwnerNodeID: "exit-active",
},
ClientConfig: json.RawMessage(`{"routes":["0.0.0.0/0"]}`),
}
var cfg map[string]any
if err := json.Unmarshal(enrichVPNClientFabricRoute(item, "entry-2", ""), &cfg); err != nil {
t.Fatalf("unmarshal enriched config: %v", err)
}
route, ok := cfg["vpn_fabric_route"].(map[string]any)
if !ok {
t.Fatalf("missing vpn_fabric_route in %#v", cfg)
}
if route["preferred_data_plane"] != "fabric_mesh" || route["fallback_data_plane"] != "backend_relay" {
t.Fatalf("unexpected data-plane route contract: %#v", route)
}
if route["selected_entry_node_id"] != "entry-2" || route["selected_exit_node_id"] != "exit-active" {
t.Fatalf("unexpected selected route endpoints: %#v", route)
}
if route["route_candidate_count"].(float64) != 8 {
t.Fatalf("route candidate count = %#v", route["route_candidate_count"])
}
candidates := route["route_candidates"].([]any)
firstCandidate := candidates[0].(map[string]any)
if firstCandidate["role"] != "preferred" || firstCandidate["entry_node_id"] != "entry-2" || firstCandidate["exit_node_id"] != "exit-active" {
t.Fatalf("preferred route candidate = %#v", firstCandidate)
}
entryPool := route["entry_pool_node_ids"].([]any)
exitPool := route["exit_pool_node_ids"].([]any)
if len(entryPool) != 2 || entryPool[0] != "entry-1" || entryPool[1] != "entry-2" {
t.Fatalf("entry pool = %#v", entryPool)
}
if len(exitPool) != 4 || exitPool[0] != "exit-policy" || exitPool[1] != "exit-active" || exitPool[2] != "node-a" || exitPool[3] != "node-b" {
t.Fatalf("exit pool = %#v", exitPool)
}
contract, ok := cfg["vpn_dataplane_contract"].(map[string]any)
if !ok {
t.Fatalf("missing vpn_dataplane_contract in %#v", cfg)
}
if contract["tunnel_type"] != "universal_ip_packet" || contract["application_protocol_agnostic"] != true {
t.Fatalf("unexpected dataplane contract: %#v", contract)
}
failover := contract["failover"].(map[string]any)
if failover["enabled"] != true || failover["alternate_route_count"].(float64) != 7 {
t.Fatalf("unexpected failover contract: %#v", failover)
}
}
func TestEnrichVPNClientFabricRoutePrefersExplicitExit(t *testing.T) {
item := VPNClientConnection{
AllowedNodeIDs: []string{"node-a", "node-b", "node-c"},
EntryNodeIDs: []string{"entry-1", "entry-2"},
ExitNodeID: "exit-policy-a",
ActiveLease: &NodeVPNAssignmentLease{
OwnerNodeID: "",
},
ClientConfig: json.RawMessage(`{"routes":["0.0.0.0/0"]}`),
}
var cfg map[string]any
if err := json.Unmarshal(enrichVPNClientFabricRoute(item, "entry-1", "node-c"), &cfg); err != nil {
t.Fatalf("unmarshal enriched config: %v", err)
}
route, ok := cfg["vpn_fabric_route"].(map[string]any)
if !ok {
t.Fatalf("missing vpn_fabric_route in %#v", cfg)
}
if route["selected_entry_node_id"] != "entry-1" {
t.Fatalf("unexpected selected entry: %#v", route["selected_entry_node_id"])
}
if route["selected_exit_node_id"] != "node-c" {
t.Fatalf("unexpected selected exit: %#v", route["selected_exit_node_id"])
}
}
func TestEnrichVPNClientEntryEndpointCandidatesAddsReportedEntryAPI(t *testing.T) {
item := VPNClientConnection{
EntryNodeIDs: []string{"entry-1"},
ClientConfig: json.RawMessage(`{
"vpn_fabric_route": {
"status": "planned",
"selected_entry_node_id": "entry-1",
"selected_exit_node_id": "exit-1"
}
}`),
}
heartbeatMetadata := json.RawMessage(`{
"mesh_endpoint_report": {
"transport": "direct_http",
"connectivity_mode": "direct",
"nat_type": "none",
"region": "test",
"peer_endpoint": "http://entry.example.test:19131",
"endpoint_candidates": [{
"endpoint_id": "public-http",
"node_id": "entry-1",
"transport": "direct_http",
"address": "http://entry.example.test:19131",
"reachability": "public",
"priority": 0
}]
}
}`)
endpoints := map[string][]map[string]any{
"entry-1": vpnEntryEndpointCandidatesFromHeartbeat("entry-1", json.RawMessage(`{"vpn_local_gateway_shortcut":true}`), heartbeatMetadata),
}
var cfg map[string]any
if err := json.Unmarshal(enrichVPNClientEntryEndpointCandidates(item, endpoints), &cfg); err != nil {
t.Fatalf("unmarshal enriched config: %v", err)
}
if cfg["vpn_entry_endpoint_candidate_count"].(float64) != 1 {
t.Fatalf("candidate count = %#v", cfg["vpn_entry_endpoint_candidate_count"])
}
candidates := cfg["vpn_entry_endpoint_candidates"].([]any)
candidate := candidates[0].(map[string]any)
if candidate["node_id"] != "entry-1" || candidate["api_base_url"] != "http://entry.example.test:19131/api/v1" {
t.Fatalf("unexpected endpoint candidate: %#v", candidate)
}
if candidate["local_gateway_shortcut"] != true {
t.Fatalf("local gateway shortcut missing: %#v", candidate)
}
if candidate["selected_entry"] != true || candidate["source"] != "node_latest_heartbeat.mesh_endpoint_report.endpoint_candidates" {
t.Fatalf("unexpected endpoint metadata: %#v", candidate)
}
}
+28 -1
View File
@@ -22,6 +22,7 @@ type Repository interface {
AssignNodeToGroup(ctx context.Context, input AssignNodeGroupInput) (ClusterNode, error)
CreateJoinToken(ctx context.Context, input CreateJoinTokenInput, tokenHash string) (NodeJoinToken, error)
ListJoinTokens(ctx context.Context, clusterID string) ([]NodeJoinToken, error)
SetJoinTokenAuthority(ctx context.Context, clusterID, tokenID string, payload json.RawMessage, signature ClusterSignature) (NodeJoinToken, error)
GetValidJoinTokenByHash(ctx context.Context, clusterID, tokenHash string) (NodeJoinToken, error)
RevokeJoinToken(ctx context.Context, input RevokeJoinTokenInput) (NodeJoinToken, error)
@@ -40,8 +41,16 @@ type Repository interface {
RecordHeartbeat(ctx context.Context, input RecordHeartbeatInput) (NodeHeartbeat, error)
ListNodeHeartbeats(ctx context.Context, clusterID, nodeID string, limit int) ([]NodeHeartbeat, error)
CreateReleaseVersion(ctx context.Context, input CreateReleaseVersionInput) (ReleaseVersion, error)
ListReleaseVersions(ctx context.Context, clusterID, product, channel string) ([]ReleaseVersion, error)
ListNodeUpdateServiceCandidates(ctx context.Context, clusterID string) ([]NodeUpdateServiceCandidate, error)
UpsertNodeUpdatePolicy(ctx context.Context, input UpsertNodeUpdatePolicyInput) (NodeUpdatePolicy, error)
GetNodeUpdatePolicy(ctx context.Context, clusterID, nodeID, product string) (NodeUpdatePolicy, error)
ReportNodeUpdateStatus(ctx context.Context, input ReportNodeUpdateStatusInput) (NodeUpdateStatus, error)
ListNodeUpdateStatuses(ctx context.Context, clusterID, nodeID string, limit int) ([]NodeUpdateStatus, error)
RevokeNodeIdentity(ctx context.Context, input RevokeNodeIdentityInput) error
DisableClusterMembership(ctx context.Context, input DisableMembershipInput) error
DeleteClusterNode(ctx context.Context, input DeleteClusterNodeInput) error
UpsertFabricTestingFlag(ctx context.Context, input UpsertFabricTestingFlagInput) (FabricTestingFlag, error)
ListFabricTestingFlags(ctx context.Context) ([]FabricTestingFlag, error)
GetEffectiveNodeTestingFlags(ctx context.Context, clusterID, nodeID string) (EffectiveNodeTestingFlags, error)
@@ -55,6 +64,22 @@ type Repository interface {
ListMeshLinks(ctx context.Context, clusterID string) ([]MeshLinkObservation, error)
CreateRouteIntent(ctx context.Context, input CreateRouteIntentInput) (MeshRouteIntent, error)
ListRouteIntents(ctx context.Context, clusterID string) ([]MeshRouteIntent, error)
ExpireRouteIntent(ctx context.Context, input RouteIntentLifecycleInput, expiresAt time.Time) (MeshRouteIntent, error)
DisableRouteIntent(ctx context.Context, input RouteIntentLifecycleInput) (MeshRouteIntent, error)
RecordFabricServiceChannelRouteFeedback(ctx context.Context, input RecordFabricServiceChannelRouteFeedbackInput) (FabricServiceChannelRouteFeedbackObservation, error)
ListFabricServiceChannelRouteFeedback(ctx context.Context, input ListFabricServiceChannelRouteFeedbackInput) ([]FabricServiceChannelRouteFeedbackObservation, error)
ExpireFabricServiceChannelRouteFeedback(ctx context.Context, input ExpireFabricServiceChannelRouteFeedbackInput) (ExpireFabricServiceChannelRouteFeedbackResult, error)
StoreFabricServiceChannelLease(ctx context.Context, input StoreFabricServiceChannelLeaseInput) (FabricServiceChannelLeaseRecord, error)
GetFabricServiceChannelLease(ctx context.Context, clusterID, channelID string) (FabricServiceChannelLeaseRecord, error)
ListFabricServiceChannelLeases(ctx context.Context, input ListFabricServiceChannelLeasesInput) ([]FabricServiceChannelLeaseRecord, error)
CleanupExpiredFabricServiceChannelLeases(ctx context.Context, clusterID string, now time.Time, limit int) (int, error)
RecordFabricServiceChannelRouteRebuildAttempt(ctx context.Context, input RecordFabricServiceChannelRouteRebuildAttemptInput) (FabricServiceChannelRouteRebuildAttempt, error)
ListFabricServiceChannelRouteRebuildAttempts(ctx context.Context, input ListFabricServiceChannelRouteRebuildAttemptsInput) ([]FabricServiceChannelRouteRebuildAttempt, error)
UpdateFabricServiceChannelRouteRebuildCorrelationSnapshot(ctx context.Context, input UpdateFabricServiceChannelRouteRebuildCorrelationSnapshotInput) error
GetFabricServiceChannelSchemaStatus(ctx context.Context, input GetFabricServiceChannelSchemaStatusInput) (FabricServiceChannelSchemaStatus, error)
UpsertFabricServiceChannelRouteRebuildAlertSilence(ctx context.Context, input SilenceFabricServiceChannelRouteRebuildAlertInput, expiresAt time.Time) (FabricServiceChannelRouteRebuildAlertSilence, error)
ListFabricServiceChannelRouteRebuildAlertSilences(ctx context.Context, clusterID string, now time.Time) ([]FabricServiceChannelRouteRebuildAlertSilence, error)
DeleteFabricServiceChannelRouteRebuildAlertSilence(ctx context.Context, input UnsilenceFabricServiceChannelRouteRebuildAlertInput) (FabricServiceChannelRouteRebuildAlertSilence, error)
ListQoSPolicies(ctx context.Context, clusterID string) ([]MeshQoSPolicy, error)
ListFabricEntryPoints(ctx context.Context, clusterID string) ([]FabricEntryPoint, error)
CreateFabricEntryPoint(ctx context.Context, input CreateFabricEntryPointInput) (FabricEntryPoint, error)
@@ -78,6 +103,7 @@ type Repository interface {
ListVPNConnectionAllowedNodes(ctx context.Context, clusterID, vpnConnectionID string) ([]VPNConnectionAllowedNode, error)
AcquireVPNConnectionLease(ctx context.Context, input AcquireVPNConnectionLeaseInput, expiresAt time.Time, fencingToken string) (VPNConnectionLease, error)
RenewVPNConnectionLease(ctx context.Context, input RenewVPNConnectionLeaseInput, expiresAt time.Time) (VPNConnectionLease, error)
RenewNodeVPNAssignmentLease(ctx context.Context, input RenewNodeVPNAssignmentLeaseInput, expiresAt time.Time) (VPNConnectionLease, error)
ReleaseVPNConnectionLease(ctx context.Context, input ReleaseVPNConnectionLeaseInput) (VPNConnectionLease, error)
FenceVPNConnectionLease(ctx context.Context, input FenceVPNConnectionLeaseInput) (VPNConnectionLease, error)
GetActiveVPNConnectionLease(ctx context.Context, clusterID, vpnConnectionID string) (VPNConnectionLease, error)
@@ -85,7 +111,8 @@ type Repository interface {
ExpireStaleVPNConnectionLeases(ctx context.Context, clusterID string, now time.Time) ([]VPNConnectionLease, error)
ListNodeVPNAssignments(ctx context.Context, clusterID, nodeID string) ([]NodeVPNAssignment, error)
ReportNodeVPNAssignmentStatus(ctx context.Context, input ReportNodeVPNAssignmentStatusInput) (NodeVPNAssignmentStatus, error)
GetVPNClientProfile(ctx context.Context, clusterID, organizationID, userID, preferredEntryNodeID, preferredExitNodeID string, generatedAt time.Time) (VPNClientProfile, error)
RecordAudit(ctx context.Context, event ClusterAuditEvent) error
ListAuditEvents(ctx context.Context, clusterID string, limit int) ([]ClusterAuditEvent, error)
ListAuditEvents(ctx context.Context, input ListAuditEventsInput) ([]ClusterAuditEvent, error)
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -40,6 +40,9 @@ func (m *Module) Name() string {
func (m *Module) RegisterRoutes(router chi.Router) {
router.Route("/node-agents", func(r chi.Router) {
r.Post("/docker-install-profile", m.dockerInstallProfile)
r.Post("/windows-install-profile", m.windowsInstallProfile)
r.Post("/linux-install-profile", m.linuxInstallProfile)
r.Post("/enroll", m.enrollAgent)
r.Post("/enrollments/{requestID}/bootstrap", m.bootstrapEnrollment)
r.Post("/register", m.registerAgent)
@@ -53,6 +56,48 @@ func (m *Module) RegisterRoutes(router chi.Router) {
})
}
func (m *Module) linuxInstallProfile(w http.ResponseWriter, r *http.Request) {
var payload clustermodule.DockerInstallProfileRequest
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
httpx.WriteError(w, http.StatusBadRequest, "invalid linux install profile payload")
return
}
profile, err := m.cluster.GetLinuxInstallProfile(r.Context(), payload)
if err != nil {
httpx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
httpx.WriteJSON(w, http.StatusOK, map[string]any{"linux_install_profile": profile})
}
func (m *Module) windowsInstallProfile(w http.ResponseWriter, r *http.Request) {
var payload clustermodule.DockerInstallProfileRequest
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
httpx.WriteError(w, http.StatusBadRequest, "invalid windows install profile payload")
return
}
profile, err := m.cluster.GetWindowsInstallProfile(r.Context(), payload)
if err != nil {
httpx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
httpx.WriteJSON(w, http.StatusOK, map[string]any{"windows_install_profile": profile})
}
func (m *Module) dockerInstallProfile(w http.ResponseWriter, r *http.Request) {
var payload clustermodule.DockerInstallProfileRequest
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
httpx.WriteError(w, http.StatusBadRequest, "invalid docker install profile payload")
return
}
profile, err := m.cluster.GetDockerInstallProfile(r.Context(), payload)
if err != nil {
httpx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
httpx.WriteJSON(w, http.StatusOK, map[string]any{"docker_install_profile": profile})
}
func (m *Module) enrollAgent(w http.ResponseWriter, r *http.Request) {
var payload struct {
ClusterID string `json:"cluster_id"`
@@ -242,6 +242,37 @@ func (m *Module) loadAdminSummary(ctx context.Context, orgID string) (AdminSumma
return AdminSummary{}, err
}
var vpnConnectionCount int64
var vpnActiveLeaseCount int64
var vpnForwardingCount int64
if err := m.db.QueryRow(ctx, `
SELECT COUNT(*)
FROM vpn_connections
WHERE organization_id = $1::uuid
AND desired_state = 'enabled'
`, orgID).Scan(&vpnConnectionCount); err != nil {
return AdminSummary{}, err
}
if err := m.db.QueryRow(ctx, `
SELECT COUNT(*)
FROM vpn_connection_leases l
INNER JOIN vpn_connections vc ON vc.id = l.vpn_connection_id
WHERE vc.organization_id = $1::uuid
AND l.status = 'active'
AND l.expires_at > NOW()
`, orgID).Scan(&vpnActiveLeaseCount); err != nil {
return AdminSummary{}, err
}
if err := m.db.QueryRow(ctx, `
SELECT COUNT(*)
FROM vpn_connection_assignment_latest_statuses s
INNER JOIN vpn_connections vc ON vc.id = s.vpn_connection_id
WHERE vc.organization_id = $1::uuid
AND COALESCE((s.status_payload->>'packet_forwarding')::boolean, false)
`, orgID).Scan(&vpnForwardingCount); err != nil {
return AdminSummary{}, err
}
auditRows, err := m.db.Query(ctx, `
SELECT ae.id::text, ae.event_type, ae.target_type, ae.target_id, ae.payload, ae.created_at
FROM audit_events ae
@@ -265,6 +296,12 @@ func (m *Module) loadAdminSummary(ctx context.Context, orgID string) (AdminSumma
if err := auditRows.Err(); err != nil {
return AdminSummary{}, err
}
if services == nil {
services = []ServiceSummary{}
}
if audit == nil {
audit = []OrgAuditEvent{}
}
return AdminSummary{
OrganizationID: orgID,
@@ -272,14 +309,34 @@ func (m *Module) loadAdminSummary(ctx context.Context, orgID string) (AdminSumma
ActiveSessionCount: activeSessionCount,
ServiceEndpoints: services,
ConnectorStatus: map[string]any{
"vpn": "not_implemented",
"connector": "not_implemented",
"vpn": map[string]any{
"enabled_connections": vpnConnectionCount,
"active_leases": vpnActiveLeaseCount,
"packet_forwarding": vpnForwardingCount,
"status": connectorStatus(vpnConnectionCount, vpnActiveLeaseCount, vpnForwardingCount),
},
"rdp": map[string]any{
"status": "resource_catalog_ready",
},
},
RecentAudit: audit,
TopologyExposure: tenantSafeTopologyExposure(),
}, nil
}
func connectorStatus(enabledConnections, activeLeases, forwarding int64) string {
if enabledConnections == 0 {
return "not_configured"
}
if forwarding > 0 {
return "active"
}
if activeLeases > 0 {
return "gateway_blocked"
}
return "waiting_for_gateway"
}
func tenantSafeTopologyExposure() string {
return "tenant_safe_no_core_mesh_topology"
}
@@ -49,6 +49,7 @@ type Resource struct {
Address string `json:"address"`
Protocol string `json:"protocol"`
SecretRef *string `json:"secret_ref,omitempty"`
HasSecret bool `json:"has_secret"`
CertificateVerificationMode string `json:"certificate_verification_mode"`
RenderQualityProfile string `json:"render_quality_profile"`
ClipboardMode string `json:"clipboard_mode"`
@@ -116,6 +117,7 @@ func (m *Module) listResources(w http.ResponseWriter, r *http.Request) {
query := `
SELECT r.id, r.organization_id, r.name, r.address, r.protocol, r.secret_ref,
r.certificate_verification_mode, r.metadata, r.created_at, r.updated_at,
EXISTS (SELECT 1 FROM resource_secrets sec WHERE sec.resource_id = r.id) AS has_secret,
COALESCE(rp.clipboard_mode, 'disabled') AS clipboard_mode,
COALESCE(rp.file_transfer_mode, 'disabled') AS file_transfer_mode
FROM resources r
@@ -500,6 +502,7 @@ func (m *Module) getByID(ctx context.Context, resourceID string) (Resource, erro
row := m.db.QueryRow(ctx, `
SELECT r.id, r.organization_id, r.name, r.address, r.protocol, r.secret_ref,
r.certificate_verification_mode, r.metadata, r.created_at, r.updated_at,
EXISTS (SELECT 1 FROM resource_secrets sec WHERE sec.resource_id = r.id) AS has_secret,
COALESCE(rp.clipboard_mode, 'disabled') AS clipboard_mode,
COALESCE(rp.file_transfer_mode, 'disabled') AS file_transfer_mode
FROM resources r
@@ -555,6 +558,7 @@ func scanResource(row rowScanner) (Resource, error) {
&resource.Metadata,
&resource.CreatedAt,
&resource.UpdatedAt,
&resource.HasSecret,
&resource.ClipboardMode,
&resource.FileTransferMode,
); err != nil {
@@ -214,9 +214,49 @@ END, prg.granted_at DESC
if err := rows.Err(); err != nil {
return "", fmt.Errorf("iterate platform role grants: %w", err)
}
if bestRole == PlatformRoleUser {
if role, ok, err := strictBootstrappedOwnerFallback(ctx, db, verifier, userID, email); err != nil {
return "", err
} else if ok {
return role, nil
}
return legacyPlatformRole(ctx, db, userID)
}
return bestRole, nil
}
func strictBootstrappedOwnerFallback(ctx context.Context, db postgresplatform.DBTX, verifier *Verifier, userID, email string) (string, bool, error) {
var role string
var bootstrappedOwnerEmail *string
var authorityState string
var rootFingerprint string
err := db.QueryRow(ctx, `
SELECT u.platform_role, ia.bootstrapped_owner_email, ia.authority_state, ia.product_root_key_fingerprint
FROM users u
CROSS JOIN installation_authority ia
WHERE u.id = $1::uuid
AND ia.id = 1
`, userID).Scan(&role, &bootstrappedOwnerEmail, &authorityState, &rootFingerprint)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return PlatformRoleUser, false, nil
}
return "", false, fmt.Errorf("query strict bootstrapped owner fallback: %w", err)
}
if bootstrappedOwnerEmail == nil ||
!strings.EqualFold(*bootstrappedOwnerEmail, email) ||
authorityState != "active" ||
rootFingerprint != verifier.RootFingerprint() {
return PlatformRoleUser, false, nil
}
switch role {
case PlatformRoleAdmin, PlatformRoleRecoveryAdmin:
return role, true, nil
default:
return PlatformRoleUser, false, nil
}
}
func legacyPlatformRole(ctx context.Context, db postgresplatform.DBTX, userID string) (string, error) {
var role string
if err := db.QueryRow(ctx, `SELECT platform_role FROM users WHERE id = $1::uuid`, userID).Scan(&role); err != nil {
+33
View File
@@ -2,6 +2,7 @@ package runtime
import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
@@ -208,6 +209,7 @@ func buildRouter(logger *slog.Logger, modules ...module.Module) http.Handler {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ready"))
})
router.Post("/mesh/v1/health", controlPlaneMeshHealth)
router.Route("/api/v1", func(r chi.Router) {
for _, mod := range modules {
@@ -218,3 +220,34 @@ func buildRouter(logger *slog.Logger, modules ...module.Module) http.Handler {
return router
}
func controlPlaneMeshHealth(w http.ResponseWriter, r *http.Request) {
var message struct {
ProtocolVersion string `json:"protocol_version"`
From struct {
ClusterID string `json:"cluster_id"`
NodeID string `json:"node_id"`
} `json:"from"`
To struct {
ClusterID string `json:"cluster_id"`
NodeID string `json:"node_id"`
} `json:"to"`
}
if err := json.NewDecoder(r.Body).Decode(&message); err != nil {
http.Error(w, "invalid mesh health message", http.StatusBadRequest)
return
}
if message.ProtocolVersion != "mesh-control-v1" || message.From.ClusterID == "" || message.From.NodeID == "" {
http.Error(w, "invalid mesh health message", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"protocol_version": "mesh-control-v1",
"accepted": true,
"by": map[string]string{
"cluster_id": message.From.ClusterID,
"node_id": "control-plane-relay",
},
})
}
@@ -0,0 +1,8 @@
DROP INDEX IF EXISTS node_update_status_reports_latest_idx;
DROP INDEX IF EXISTS release_artifacts_match_idx;
DROP INDEX IF EXISTS release_versions_lookup_idx;
DROP TABLE IF EXISTS node_update_status_reports;
DROP TABLE IF EXISTS node_update_desired_policies;
DROP TABLE IF EXISTS release_artifacts;
DROP TABLE IF EXISTS release_versions;
@@ -0,0 +1,74 @@
CREATE TABLE IF NOT EXISTS release_versions (
id UUID PRIMARY KEY,
cluster_id UUID NOT NULL REFERENCES clusters(id) ON DELETE CASCADE,
product TEXT NOT NULL,
version TEXT NOT NULL,
channel TEXT NOT NULL DEFAULT 'dev',
status TEXT NOT NULL DEFAULT 'active',
compatibility JSONB NOT NULL DEFAULT '{}'::jsonb,
changelog TEXT,
created_by_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
authority_payload JSONB NOT NULL DEFAULT '{}'::jsonb,
authority_signature JSONB NOT NULL DEFAULT '{}'::jsonb,
UNIQUE (cluster_id, product, version, channel)
);
CREATE TABLE IF NOT EXISTS release_artifacts (
id UUID PRIMARY KEY,
release_id UUID NOT NULL REFERENCES release_versions(id) ON DELETE CASCADE,
cluster_id UUID NOT NULL REFERENCES clusters(id) ON DELETE CASCADE,
product TEXT NOT NULL,
version TEXT NOT NULL,
os TEXT NOT NULL,
arch TEXT NOT NULL,
install_type TEXT NOT NULL,
kind TEXT NOT NULL,
url TEXT NOT NULL,
sha256 TEXT NOT NULL,
size_bytes BIGINT NOT NULL DEFAULT 0,
signature TEXT,
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (release_id, os, arch, install_type, kind)
);
CREATE TABLE IF NOT EXISTS node_update_desired_policies (
cluster_id UUID NOT NULL REFERENCES clusters(id) ON DELETE CASCADE,
node_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
product TEXT NOT NULL,
channel TEXT NOT NULL DEFAULT 'dev',
target_version TEXT,
strategy TEXT NOT NULL DEFAULT 'manual',
enabled BOOLEAN NOT NULL DEFAULT false,
rollback_allowed BOOLEAN NOT NULL DEFAULT true,
health_window_seconds INTEGER NOT NULL DEFAULT 180,
updated_by_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (cluster_id, node_id, product)
);
CREATE TABLE IF NOT EXISTS node_update_status_reports (
id UUID PRIMARY KEY,
cluster_id UUID NOT NULL REFERENCES clusters(id) ON DELETE CASCADE,
node_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
product TEXT NOT NULL,
current_version TEXT NOT NULL DEFAULT '',
target_version TEXT NOT NULL DEFAULT '',
phase TEXT NOT NULL,
status TEXT NOT NULL,
attempt_id TEXT NOT NULL DEFAULT '',
error_message TEXT,
rollback_version TEXT,
payload JSONB NOT NULL DEFAULT '{}'::jsonb,
observed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS release_versions_lookup_idx
ON release_versions (cluster_id, product, channel, status, created_at DESC);
CREATE INDEX IF NOT EXISTS release_artifacts_match_idx
ON release_artifacts (release_id, os, arch, install_type);
CREATE INDEX IF NOT EXISTS node_update_status_reports_latest_idx
ON node_update_status_reports (cluster_id, node_id, product, observed_at DESC);
@@ -0,0 +1,26 @@
DROP VIEW IF EXISTS cluster_admin_summaries;
CREATE VIEW cluster_admin_summaries AS
SELECT
c.id AS cluster_id,
c.slug,
c.name,
c.status,
c.region,
COALESCE(cas.authority_state, 'authoritative') AS authority_state,
COALESCE(cas.mutation_mode, 'normal') AS mutation_mode,
ca.key_algorithm AS cluster_key_algorithm,
ca.public_key_fingerprint AS cluster_key_fingerprint,
COUNT(DISTINCT cm.node_id) AS node_count,
COUNT(DISTINCT CASE WHEN n.health_status = 'healthy' THEN n.id END) AS healthy_node_count,
COUNT(DISTINCT CASE WHEN njr.status = 'pending' THEN njr.id END) AS pending_join_count,
COUNT(DISTINCT nra.id) AS active_role_assignment_count,
MAX(n.last_seen_at) AS last_node_seen_at
FROM clusters c
LEFT JOIN cluster_authority_states cas ON cas.cluster_id = c.id
LEFT JOIN cluster_authorities ca ON ca.cluster_id = c.id
LEFT JOIN cluster_memberships cm ON cm.cluster_id = c.id
LEFT JOIN nodes n ON n.id = cm.node_id
LEFT JOIN node_join_requests njr ON njr.cluster_id = c.id
LEFT JOIN node_role_assignments nra ON nra.cluster_id = c.id AND nra.status = 'active'
GROUP BY c.id, c.slug, c.name, c.status, c.region, cas.authority_state, cas.mutation_mode, ca.key_algorithm, ca.public_key_fingerprint;
@@ -0,0 +1,29 @@
DROP VIEW IF EXISTS cluster_admin_summaries;
CREATE VIEW cluster_admin_summaries AS
SELECT
c.id AS cluster_id,
c.slug,
c.name,
c.status,
c.region,
COALESCE(cas.authority_state, 'authoritative') AS authority_state,
COALESCE(cas.mutation_mode, 'normal') AS mutation_mode,
ca.key_algorithm AS cluster_key_algorithm,
ca.public_key_fingerprint AS cluster_key_fingerprint,
COUNT(DISTINCT cm.node_id) AS node_count,
COUNT(DISTINCT CASE
WHEN n.health_status = 'healthy'
AND n.last_seen_at >= NOW() - '1 minute'::interval THEN n.id
END) AS healthy_node_count,
COUNT(DISTINCT CASE WHEN njr.status = 'pending' THEN njr.id END) AS pending_join_count,
COUNT(DISTINCT nra.id) AS active_role_assignment_count,
MAX(n.last_seen_at) AS last_node_seen_at
FROM clusters c
LEFT JOIN cluster_authority_states cas ON cas.cluster_id = c.id
LEFT JOIN cluster_authorities ca ON ca.cluster_id = c.id
LEFT JOIN cluster_memberships cm ON cm.cluster_id = c.id
LEFT JOIN nodes n ON n.id = cm.node_id
LEFT JOIN node_join_requests njr ON njr.cluster_id = c.id
LEFT JOIN node_role_assignments nra ON nra.cluster_id = c.id AND nra.status = 'active'
GROUP BY c.id, c.slug, c.name, c.status, c.region, cas.authority_state, cas.mutation_mode, ca.key_algorithm, ca.public_key_fingerprint;
@@ -0,0 +1,2 @@
DROP TABLE IF EXISTS fabric_service_channel_route_feedback_latest;
DROP TABLE IF EXISTS fabric_service_channel_route_feedback_observations;
@@ -0,0 +1,45 @@
CREATE TABLE IF NOT EXISTS fabric_service_channel_route_feedback_observations (
id UUID PRIMARY KEY,
cluster_id UUID NOT NULL REFERENCES clusters(id) ON DELETE CASCADE,
reporter_node_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
route_id TEXT NOT NULL,
service_class TEXT NOT NULL,
feedback_status TEXT NOT NULL,
score_adjustment INTEGER NOT NULL DEFAULT 0,
reasons TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
last_error TEXT NOT NULL DEFAULT '',
consecutive_failures INTEGER NOT NULL DEFAULT 0,
stall_count INTEGER NOT NULL DEFAULT 0,
last_send_duration_ms BIGINT NOT NULL DEFAULT 0,
payload JSONB NOT NULL DEFAULT '{}'::JSONB,
observed_at TIMESTAMPTZ NOT NULL,
expires_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_fsc_route_feedback_observed
ON fabric_service_channel_route_feedback_observations (cluster_id, reporter_node_id, service_class, observed_at DESC);
CREATE INDEX IF NOT EXISTS idx_fsc_route_feedback_route
ON fabric_service_channel_route_feedback_observations (cluster_id, route_id, observed_at DESC);
CREATE TABLE IF NOT EXISTS fabric_service_channel_route_feedback_latest (
cluster_id UUID NOT NULL REFERENCES clusters(id) ON DELETE CASCADE,
reporter_node_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
route_id TEXT NOT NULL,
observation_id UUID NOT NULL REFERENCES fabric_service_channel_route_feedback_observations(id) ON DELETE CASCADE,
service_class TEXT NOT NULL,
feedback_status TEXT NOT NULL,
score_adjustment INTEGER NOT NULL DEFAULT 0,
reasons TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
last_error TEXT NOT NULL DEFAULT '',
consecutive_failures INTEGER NOT NULL DEFAULT 0,
stall_count INTEGER NOT NULL DEFAULT 0,
last_send_duration_ms BIGINT NOT NULL DEFAULT 0,
payload JSONB NOT NULL DEFAULT '{}'::JSONB,
observed_at TIMESTAMPTZ NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
PRIMARY KEY (cluster_id, reporter_node_id, route_id)
);
CREATE INDEX IF NOT EXISTS idx_fsc_route_feedback_latest_active
ON fabric_service_channel_route_feedback_latest (cluster_id, reporter_node_id, service_class, expires_at DESC);
@@ -0,0 +1 @@
DROP TABLE IF EXISTS fabric_service_channel_route_rebuild_attempts;
@@ -0,0 +1,43 @@
CREATE TABLE IF NOT EXISTS fabric_service_channel_route_rebuild_attempts (
id UUID PRIMARY KEY,
cluster_id UUID NOT NULL REFERENCES clusters(id) ON DELETE CASCADE,
reporter_node_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
service_class TEXT NOT NULL,
route_id TEXT NOT NULL,
replacement_route_id TEXT NOT NULL DEFAULT '',
rebuild_request_id TEXT NOT NULL,
rebuild_status TEXT NOT NULL,
rebuild_reason TEXT NOT NULL DEFAULT '',
rebuild_attempt INTEGER NOT NULL DEFAULT 0,
decision_source TEXT NOT NULL,
outcome TEXT NOT NULL,
generation TEXT NOT NULL DEFAULT '',
policy_fingerprint TEXT NOT NULL DEFAULT '',
observed_policy_fingerprint TEXT NOT NULL DEFAULT '',
observed_route_generation TEXT NOT NULL DEFAULT '',
effective_route_generation TEXT NOT NULL DEFAULT '',
feedback_status TEXT NOT NULL DEFAULT '',
feedback_score_adjustment INTEGER NOT NULL DEFAULT 0,
feedback_effective_score_adjustment INTEGER NOT NULL DEFAULT 0,
feedback_reasons TEXT[] NOT NULL DEFAULT '{}',
last_error TEXT NOT NULL DEFAULT '',
consecutive_failures INTEGER NOT NULL DEFAULT 0,
stall_count INTEGER NOT NULL DEFAULT 0,
last_send_duration_ms BIGINT NOT NULL DEFAULT 0,
quality_window_sample_count INTEGER NOT NULL DEFAULT 0,
quality_window_failure_count INTEGER NOT NULL DEFAULT 0,
quality_window_drop_count INTEGER NOT NULL DEFAULT 0,
quality_window_slow_count INTEGER NOT NULL DEFAULT 0,
old_hops TEXT[] NOT NULL DEFAULT '{}',
replacement_hops TEXT[] NOT NULL DEFAULT '{}',
payload JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (cluster_id, reporter_node_id, service_class, route_id, rebuild_request_id)
);
CREATE INDEX IF NOT EXISTS idx_fsc_rebuild_attempts_cluster_reporter_updated
ON fabric_service_channel_route_rebuild_attempts (cluster_id, reporter_node_id, updated_at DESC);
CREATE INDEX IF NOT EXISTS idx_fsc_rebuild_attempts_cluster_route_updated
ON fabric_service_channel_route_rebuild_attempts (cluster_id, route_id, updated_at DESC);
@@ -0,0 +1 @@
DROP TABLE IF EXISTS fabric_service_channel_rebuild_alert_silences;
@@ -0,0 +1,17 @@
CREATE TABLE IF NOT EXISTS fabric_service_channel_rebuild_alert_silences (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
cluster_id UUID NOT NULL REFERENCES clusters(id) ON DELETE CASCADE,
reporter_node_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
route_id TEXT NOT NULL,
guard_status TEXT NOT NULL,
generation TEXT NOT NULL DEFAULT '',
reason TEXT NOT NULL DEFAULT '',
created_by_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
expires_at TIMESTAMPTZ NOT NULL,
payload JSONB NOT NULL DEFAULT '{}'::jsonb,
UNIQUE (cluster_id, reporter_node_id, route_id, guard_status, generation)
);
CREATE INDEX IF NOT EXISTS idx_fsc_rebuild_alert_silences_active
ON fabric_service_channel_rebuild_alert_silences (cluster_id, expires_at DESC);
@@ -0,0 +1,23 @@
DROP INDEX IF EXISTS idx_fsc_rebuild_attempts_cluster_guard_updated;
ALTER TABLE fabric_service_channel_route_rebuild_attempts
DROP COLUMN IF EXISTS correlation_snapshot_at,
DROP COLUMN IF EXISTS correlation_timeline,
DROP COLUMN IF EXISTS guard_traffic_deadline_seconds,
DROP COLUMN IF EXISTS guard_transition_deadline_seconds,
DROP COLUMN IF EXISTS guard_reason,
DROP COLUMN IF EXISTS guard_severity,
DROP COLUMN IF EXISTS guard_status,
DROP COLUMN IF EXISTS post_rebuild_send_flow_dropped,
DROP COLUMN IF EXISTS post_rebuild_send_flow_packets,
DROP COLUMN IF EXISTS post_rebuild_send_failures,
DROP COLUMN IF EXISTS post_rebuild_send_packets,
DROP COLUMN IF EXISTS post_rebuild_selected_route_id,
DROP COLUMN IF EXISTS node_route_generation_matched,
DROP COLUMN IF EXISTS node_route_generation_withdrawn_at,
DROP COLUMN IF EXISTS node_route_generation_applied_at,
DROP COLUMN IF EXISTS node_route_generation_status,
DROP COLUMN IF EXISTS node_transition_matched,
DROP COLUMN IF EXISTS node_transition_observed_at,
DROP COLUMN IF EXISTS node_transition_generation,
DROP COLUMN IF EXISTS node_transition_status;
@@ -0,0 +1,24 @@
ALTER TABLE fabric_service_channel_route_rebuild_attempts
ADD COLUMN IF NOT EXISTS node_transition_status TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS node_transition_generation TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS node_transition_observed_at TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS node_transition_matched BOOLEAN NOT NULL DEFAULT FALSE,
ADD COLUMN IF NOT EXISTS node_route_generation_status TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS node_route_generation_applied_at TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS node_route_generation_withdrawn_at TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS node_route_generation_matched BOOLEAN NOT NULL DEFAULT FALSE,
ADD COLUMN IF NOT EXISTS post_rebuild_selected_route_id TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS post_rebuild_send_packets BIGINT NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS post_rebuild_send_failures BIGINT NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS post_rebuild_send_flow_packets BIGINT NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS post_rebuild_send_flow_dropped BIGINT NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS guard_status TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS guard_severity TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS guard_reason TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS guard_transition_deadline_seconds BIGINT NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS guard_traffic_deadline_seconds BIGINT NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS correlation_timeline JSONB NOT NULL DEFAULT '[]'::jsonb,
ADD COLUMN IF NOT EXISTS correlation_snapshot_at TIMESTAMPTZ;
CREATE INDEX IF NOT EXISTS idx_fsc_rebuild_attempts_cluster_guard_updated
ON fabric_service_channel_route_rebuild_attempts (cluster_id, guard_severity, guard_status, updated_at DESC);
@@ -0,0 +1 @@
DROP TABLE IF EXISTS fabric_service_channel_leases;
@@ -0,0 +1,19 @@
CREATE TABLE IF NOT EXISTS fabric_service_channel_leases (
cluster_id UUID NOT NULL REFERENCES clusters(id) ON DELETE CASCADE,
channel_id UUID NOT NULL,
token_hash TEXT NOT NULL,
resource_id TEXT NOT NULL DEFAULT '',
service_class TEXT NOT NULL,
selected_entry_node_id UUID NULL REFERENCES nodes(id) ON DELETE SET NULL,
expires_at TIMESTAMPTZ NOT NULL,
lease JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (cluster_id, channel_id)
);
CREATE INDEX IF NOT EXISTS fabric_service_channel_leases_cluster_expires_idx
ON fabric_service_channel_leases(cluster_id, expires_at);
CREATE INDEX IF NOT EXISTS fabric_service_channel_leases_entry_idx
ON fabric_service_channel_leases(cluster_id, selected_entry_node_id, expires_at);
@@ -0,0 +1,17 @@
DELETE FROM mesh_qos_policies
WHERE service_class IN ('remote_workspace', 'video')
AND metadata->>'fabric_service_channel' = 'true';
ALTER TABLE mesh_route_intents
DROP CONSTRAINT IF EXISTS mesh_route_intents_service_class_check;
ALTER TABLE mesh_route_intents
ADD CONSTRAINT mesh_route_intents_service_class_check
CHECK (service_class IN ('input', 'control', 'synthetic', 'render', 'clipboard', 'file_transfer', 'vpn_packets', 'telemetry'));
ALTER TABLE mesh_qos_policies
DROP CONSTRAINT IF EXISTS mesh_qos_policies_service_class_check;
ALTER TABLE mesh_qos_policies
ADD CONSTRAINT mesh_qos_policies_service_class_check
CHECK (service_class IN ('input', 'control', 'synthetic', 'render', 'clipboard', 'file_transfer', 'vpn_packets', 'telemetry'));
@@ -0,0 +1,29 @@
ALTER TABLE mesh_route_intents
DROP CONSTRAINT IF EXISTS mesh_route_intents_service_class_check;
ALTER TABLE mesh_route_intents
ADD CONSTRAINT mesh_route_intents_service_class_check
CHECK (service_class IN ('input', 'control', 'synthetic', 'render', 'clipboard', 'file_transfer', 'vpn_packets', 'remote_workspace', 'video', 'telemetry'));
ALTER TABLE mesh_qos_policies
DROP CONSTRAINT IF EXISTS mesh_qos_policies_service_class_check;
ALTER TABLE mesh_qos_policies
ADD CONSTRAINT mesh_qos_policies_service_class_check
CHECK (service_class IN ('input', 'control', 'synthetic', 'render', 'clipboard', 'file_transfer', 'vpn_packets', 'remote_workspace', 'video', 'telemetry'));
INSERT INTO mesh_qos_policies (
cluster_id, service_class, priority, reliability_mode, drop_policy, bandwidth_policy, metadata
)
SELECT c.id, 'remote_workspace', 20, 'adaptive', 'adaptive', '{}'::jsonb,
'{"default":true,"fabric_service_channel":true,"interactive":true}'::jsonb
FROM clusters c
ON CONFLICT (cluster_id, service_class) DO NOTHING;
INSERT INTO mesh_qos_policies (
cluster_id, service_class, priority, reliability_mode, drop_policy, bandwidth_policy, metadata
)
SELECT c.id, 'video', 40, 'adaptive', 'adaptive', '{}'::jsonb,
'{"default":true,"fabric_service_channel":true,"adaptive":true}'::jsonb
FROM clusters c
ON CONFLICT (cluster_id, service_class) DO NOTHING;