рабочий вариант, но скороть 10 МБит
This commit is contained in:
@@ -28,6 +28,9 @@ func (m *Module) RegisterRoutes(router chi.Router) {
|
||||
r.Post("/bootstrap-owner", m.handleBootstrapOwner)
|
||||
})
|
||||
router.Route("/auth", func(r chi.Router) {
|
||||
r.Get("/ui/login", m.handleLoginHTMLPage)
|
||||
r.Post("/ui/login", m.handleLoginHTML)
|
||||
r.Get("/ui/vpn-download", m.handleVPNDownloadHTML)
|
||||
r.Post("/login", m.handleLogin)
|
||||
r.Post("/refresh", m.handleRefresh)
|
||||
r.Post("/sessions/revoke", m.handleRevokeAuthSession)
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/example/remote-access-platform/backend/internal/platform/authority"
|
||||
"github.com/example/remote-access-platform/backend/internal/platform/httpx"
|
||||
)
|
||||
|
||||
type loginHTMLPage struct {
|
||||
Email string
|
||||
Error string
|
||||
}
|
||||
|
||||
type vpnDownloadHTMLPage struct {
|
||||
ActorUserID string
|
||||
AndroidAPK string
|
||||
AndroidAPI string
|
||||
BuildInfo string
|
||||
Version string
|
||||
VersionCode string
|
||||
BuildType string
|
||||
Size string
|
||||
SHA256 string
|
||||
PublishedAt string
|
||||
}
|
||||
|
||||
func (m *Module) handleLoginHTMLPage(w http.ResponseWriter, r *http.Request) {
|
||||
m.renderLoginHTML(w, loginHTMLPage{
|
||||
Email: strings.TrimSpace(r.URL.Query().Get("email")),
|
||||
Error: strings.TrimSpace(r.URL.Query().Get("error")),
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Module) handleLoginHTML(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
m.renderLoginHTML(w, loginHTMLPage{Error: "Не удалось прочитать форму входа."})
|
||||
return
|
||||
}
|
||||
email := strings.TrimSpace(r.FormValue("email"))
|
||||
result, err := m.service.Login(r.Context(), LoginCommand{
|
||||
Email: email,
|
||||
Password: r.FormValue("password"),
|
||||
DeviceFingerprint: "web-control-html5",
|
||||
DeviceLabel: "HTML5 control panel",
|
||||
TrustDevice: true,
|
||||
})
|
||||
if err != nil {
|
||||
_, message := m.service.MapError(err)
|
||||
m.renderLoginHTML(w, loginHTMLPage{Email: email, Error: message})
|
||||
return
|
||||
}
|
||||
|
||||
target := "/api/v1/auth/ui/vpn-download?actor_user_id=" + template.URLQueryEscaper(result.User.ID)
|
||||
switch strings.TrimSpace(result.User.PlatformRole) {
|
||||
case authority.PlatformRoleAdmin, authority.PlatformRoleRecoveryAdmin:
|
||||
target = "/api/v1/ui/admin?actor_user_id=" + template.URLQueryEscaper(result.User.ID)
|
||||
}
|
||||
http.Redirect(w, r, target, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (m *Module) handleVPNDownloadHTML(w http.ResponseWriter, r *http.Request) {
|
||||
page := vpnDownloadHTMLPage{
|
||||
ActorUserID: strings.TrimSpace(r.URL.Query().Get("actor_user_id")),
|
||||
AndroidAPK: "/downloads/rap-android-vpn-latest-debug.apk",
|
||||
AndroidAPI: "/api/v1/downloads/rap-android-vpn-latest-debug.apk",
|
||||
BuildInfo: "/downloads/rap-android-vpn-build.json",
|
||||
}
|
||||
page.loadAndroidBuildInfo()
|
||||
page.applyAndroidDownloadPaths()
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
if err := vpnDownloadHTMLTemplate.Execute(w, page); err != nil {
|
||||
httpx.WriteError(w, http.StatusInternalServerError, "render vpn download page")
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Module) renderLoginHTML(w http.ResponseWriter, page loginHTMLPage) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
if err := loginHTMLTemplate.Execute(w, page); err != nil {
|
||||
httpx.WriteError(w, http.StatusInternalServerError, "render login page")
|
||||
}
|
||||
}
|
||||
|
||||
func (p *vpnDownloadHTMLPage) loadAndroidBuildInfo() {
|
||||
var manifest struct {
|
||||
Version struct {
|
||||
Name string `json:"name"`
|
||||
Code string `json:"code"`
|
||||
} `json:"version"`
|
||||
Build struct {
|
||||
Type string `json:"type"`
|
||||
} `json:"build"`
|
||||
Published struct {
|
||||
SHA256 string `json:"sha256"`
|
||||
SizeBytes int64 `json:"size_bytes"`
|
||||
TimestampUTC string `json:"timestamp_utc"`
|
||||
} `json:"published"`
|
||||
}
|
||||
releaseDir := strings.TrimSpace(os.Getenv("RAP_RELEASE_DIR"))
|
||||
if releaseDir == "" {
|
||||
releaseDir = "/tmp/rap-release"
|
||||
}
|
||||
data, err := os.ReadFile(filepath.Join(releaseDir, "rap-android-vpn-build.json"))
|
||||
if err != nil || json.Unmarshal(data, &manifest) != nil {
|
||||
return
|
||||
}
|
||||
p.Version = strings.TrimSpace(manifest.Version.Name)
|
||||
p.VersionCode = strings.TrimSpace(manifest.Version.Code)
|
||||
p.BuildType = strings.TrimSpace(manifest.Build.Type)
|
||||
p.SHA256 = strings.TrimSpace(manifest.Published.SHA256)
|
||||
p.PublishedAt = strings.TrimSpace(manifest.Published.TimestampUTC)
|
||||
if manifest.Published.SizeBytes > 0 {
|
||||
p.Size = fmt.Sprintf("%.1f MB", float64(manifest.Published.SizeBytes)/1024/1024)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *vpnDownloadHTMLPage) applyAndroidDownloadPaths() {
|
||||
version := strings.TrimSpace(p.Version)
|
||||
buildType := strings.TrimSpace(p.BuildType)
|
||||
if version == "" || buildType == "" {
|
||||
return
|
||||
}
|
||||
fileName := "rap-android-vpn-" + version + "-" + buildType + ".apk"
|
||||
p.AndroidAPK = "/downloads/releases/" + version + "/" + fileName
|
||||
p.AndroidAPI = "/api/v1/downloads/releases/" + version + "/" + fileName
|
||||
}
|
||||
|
||||
var loginHTMLTemplate = template.Must(template.New("login-html").Parse(`<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Вход в панель фермы</title>
|
||||
<style>
|
||||
:root{color-scheme:light;--bg:#eef2ec;--panel:#fffdf7;--ink:#102018;--muted:#66716a;--line:#dce2d8;--accent:#1f6b4c;--danger:#a9443b;--soft:#f6f4ea}
|
||||
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font-family:Inter,Segoe UI,Arial,sans-serif;font-size:15px;line-height:1.45}
|
||||
main{min-height:100vh;display:grid;place-items:center;padding:32px}
|
||||
.shell{width:min(460px,100%)}
|
||||
h1{margin:0 0 8px;font-size:26px;letter-spacing:0}.lead{margin:0 0 22px;color:var(--muted)}
|
||||
form,.note{background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:18px;box-shadow:0 14px 34px rgba(20,35,25,.08)}
|
||||
label{display:block;margin:0 0 14px;font-weight:700;font-size:13px;color:#3d4a42}
|
||||
input{width:100%;margin-top:7px;border:1px solid var(--line);border-radius:6px;background:#fff;color:var(--ink);padding:12px 13px;font:inherit}
|
||||
input:focus{outline:2px solid rgba(31,107,76,.22);border-color:var(--accent)}
|
||||
button{width:100%;border:0;border-radius:6px;background:var(--accent);color:white;padding:12px 14px;font:700 15px Inter,Segoe UI,Arial,sans-serif;cursor:pointer}
|
||||
.error{margin:0 0 14px;padding:10px 12px;border-radius:6px;background:#fff0ed;color:var(--danger);font-weight:700}
|
||||
.note{margin-top:10px;background:var(--soft);color:var(--muted);font-size:13px;box-shadow:none}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<section class="shell" aria-labelledby="title">
|
||||
<h1 id="title">Вход в панель фермы</h1>
|
||||
<p class="lead">Панель открывает администрирование только для роли администратора. Обычный пользователь получит страницу установки VPN.</p>
|
||||
<form method="post" action="/api/v1/auth/ui/login">
|
||||
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
|
||||
<label>Email или логин
|
||||
<input name="email" value="{{.Email}}" autocomplete="username" required autofocus>
|
||||
</label>
|
||||
<label>Пароль
|
||||
<input name="password" type="password" autocomplete="current-password" required>
|
||||
</label>
|
||||
<button type="submit">Войти</button>
|
||||
</form>
|
||||
<p class="note">Форма серверная HTML5: без клиентской авторизации и без тяжелой логики в браузере.</p>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>`))
|
||||
|
||||
var vpnDownloadHTMLTemplate = template.Must(template.New("vpn-download-html").Parse(`<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Установка VPN</title>
|
||||
<style>
|
||||
:root{color-scheme:light;--bg:#eef2ec;--panel:#fffdf7;--ink:#102018;--muted:#66716a;--line:#dce2d8;--accent:#1f6b4c;--soft:#f6f4ea}
|
||||
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font-family:Inter,Segoe UI,Arial,sans-serif;font-size:15px;line-height:1.45}
|
||||
main{min-height:100vh;display:grid;place-items:center;padding:32px}.shell{width:min(620px,100%)}
|
||||
h1{margin:0 0 8px;font-size:26px;letter-spacing:0}.lead{margin:0 0 22px;color:var(--muted)}
|
||||
.panel{background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:18px;box-shadow:0 14px 34px rgba(20,35,25,.08)}
|
||||
.row{display:flex;gap:12px;align-items:center;justify-content:space-between;border:1px solid var(--line);border-radius:7px;padding:14px;margin-top:12px;background:white}
|
||||
.title{font-weight:800}.meta{color:var(--muted);font-size:13px;margin-top:2px}
|
||||
a.button{display:inline-flex;align-items:center;justify-content:center;min-width:130px;border-radius:6px;background:var(--accent);color:white;text-decoration:none;padding:11px 13px;font-weight:800}
|
||||
a.secondary{background:white;color:var(--accent);border:1px solid var(--accent)}
|
||||
a.link{color:var(--accent);font-weight:700}.note{margin-top:12px;background:var(--soft);border:1px solid var(--line);border-radius:7px;padding:12px;color:var(--muted);font-size:13px}
|
||||
.facts{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;margin-top:12px}
|
||||
.fact{border:1px solid var(--line);border-radius:7px;background:white;padding:10px;min-width:0}
|
||||
.fact b{display:block;font-size:12px;color:#3d4a42}.fact span{display:block;margin-top:3px;overflow-wrap:anywhere;font-weight:800}
|
||||
.actions{display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end}
|
||||
@media(max-width:560px){.row{display:block}.actions{display:grid;margin-top:12px}a.button{width:100%}.facts{grid-template-columns:1fr}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<section class="shell" aria-labelledby="title">
|
||||
<h1 id="title">Установка VPN</h1>
|
||||
<p class="lead">Для вашей роли доступен установщик Android VPN. Узел Android после установки работает с фермой по протоколу фермы.</p>
|
||||
<div class="panel">
|
||||
<div class="row">
|
||||
<div><div class="title">Android VPN{{if .Version}} {{.Version}}{{end}}</div><div class="meta">актуальный APK установщика{{if .BuildType}} · {{.BuildType}}{{end}}</div></div>
|
||||
<div class="actions">
|
||||
<a class="button" href="{{.AndroidAPK}}" type="application/vnd.android.package-archive">Скачать APK</a>
|
||||
<a class="button secondary" href="{{.AndroidAPI}}" type="application/vnd.android.package-archive">Резерв</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="facts">
|
||||
<div class="fact"><b>Версия</b><span>{{if .Version}}{{.Version}}{{else}}не указана{{end}}</span></div>
|
||||
<div class="fact"><b>Код</b><span>{{if .VersionCode}}{{.VersionCode}}{{else}}не указан{{end}}</span></div>
|
||||
<div class="fact"><b>Размер</b><span>{{if .Size}}{{.Size}}{{else}}не указан{{end}}</span></div>
|
||||
<div class="fact"><b>SHA256</b><span>{{if .SHA256}}{{.SHA256}}{{else}}не указан{{end}}</span></div>
|
||||
<div class="fact"><b>Публикация</b><span>{{if .PublishedAt}}{{.PublishedAt}}{{else}}не указана{{end}}</span></div>
|
||||
<div class="fact"><b>Путь</b><span>{{.AndroidAPK}}</span></div>
|
||||
</div>
|
||||
<div class="note">Данные сборки: <a class="link" href="{{.BuildInfo}}">manifest JSON</a>{{if .ActorUserID}}. Пользователь: {{.ActorUserID}}{{end}}</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>`))
|
||||
@@ -453,7 +453,7 @@ func (s *Service) installationStatusFromRecord(record *InstallationAuthorityStat
|
||||
if record == nil {
|
||||
record = &InstallationAuthorityState{AuthorityState: "unbootstrapped"}
|
||||
}
|
||||
mode := authority.ModeLegacy
|
||||
mode := authority.ModeCompat
|
||||
strict := false
|
||||
rootFingerprint := ""
|
||||
insecureAllowed := false
|
||||
|
||||
@@ -53,10 +53,8 @@ const (
|
||||
FabricServiceClassRemoteWorkspace = "remote_workspace"
|
||||
FabricServiceClassFileTransfer = "file_transfer"
|
||||
FabricServiceClassVideo = "video"
|
||||
FabricServiceClassPlatformAdmin = "platform_admin"
|
||||
FabricServiceClassClusterAdmin = "cluster_admin"
|
||||
FabricServiceClassOrganization = "organization_portal"
|
||||
FabricServiceClassUserPortal = "user_portal"
|
||||
FabricServiceClassAdminIngress = "admin-ingress"
|
||||
FabricServiceClassPublicIngress = "public-ingress"
|
||||
|
||||
FabricChannelControl = "control"
|
||||
FabricChannelInteractive = "interactive"
|
||||
@@ -66,27 +64,21 @@ const (
|
||||
)
|
||||
|
||||
var allowedNodeRoles = map[string]struct{}{
|
||||
"public-ingress": {},
|
||||
"admin-ingress": {},
|
||||
"global-admin-runtime": {},
|
||||
"cluster-admin-runtime": {},
|
||||
"organization-portal-runtime": {},
|
||||
"user-portal-runtime": {},
|
||||
"identity-runtime": {},
|
||||
"policy-authority": {},
|
||||
"audit-sink": {},
|
||||
"entry-node": {},
|
||||
"relay-node": {},
|
||||
"core-mesh": {},
|
||||
"rdp-worker": {},
|
||||
"vnc-worker": {},
|
||||
"vpn-exit": {},
|
||||
"vpn-connector": {},
|
||||
"vpn-client": {},
|
||||
"ipv4-egress": {},
|
||||
"file-storage-cache": {},
|
||||
"update-cache": {},
|
||||
"video-relay": {},
|
||||
"public-ingress": {},
|
||||
"admin-ingress": {},
|
||||
"entry-node": {},
|
||||
"relay-node": {},
|
||||
"core-mesh": {},
|
||||
"rdp-worker": {},
|
||||
"vnc-worker": {},
|
||||
"vpn-exit": {},
|
||||
"vpn-connector": {},
|
||||
"vpn-client": {},
|
||||
"ipv4-ingress": {},
|
||||
"ipv4-egress": {},
|
||||
"file-storage-cache": {},
|
||||
"update-cache": {},
|
||||
"video-relay": {},
|
||||
}
|
||||
|
||||
type Cluster struct {
|
||||
@@ -163,8 +155,7 @@ type DockerInstallProfileRequest struct {
|
||||
type DockerInstallProfile struct {
|
||||
SchemaVersion string `json:"schema_version"`
|
||||
ClusterID string `json:"cluster_id"`
|
||||
BackendURL string `json:"backend_url"`
|
||||
ControlPlaneEndpoints []string `json:"control_plane_endpoints,omitempty"`
|
||||
ClusterAuthorityPublicKey string `json:"cluster_authority_public_key,omitempty"`
|
||||
ArtifactEndpoints []string `json:"artifact_endpoints,omitempty"`
|
||||
FabricRegistryRecords json.RawMessage `json:"fabric_registry_records,omitempty"`
|
||||
DockerImageArtifact *DockerArtifact `json:"docker_image_artifact,omitempty"`
|
||||
@@ -179,12 +170,12 @@ type DockerInstallProfile struct {
|
||||
Replace bool `json:"replace"`
|
||||
DockerVPNGatewayEnabled bool `json:"docker_vpn_gateway_enabled"`
|
||||
WorkloadSupervisionEnabled bool `json:"workload_supervision_enabled"`
|
||||
MeshSyntheticRuntimeEnabled bool `json:"mesh_synthetic_runtime_enabled"`
|
||||
FabricRuntimeEnabled bool `json:"fabric_runtime_enabled"`
|
||||
MeshProductionForwardingEnabled bool `json:"mesh_production_forwarding_enabled"`
|
||||
MeshListenAddr string `json:"mesh_listen_addr,omitempty"`
|
||||
MeshListenPortMode string `json:"mesh_listen_port_mode,omitempty"`
|
||||
MeshListenAutoPortStart int `json:"mesh_listen_auto_port_start,omitempty"`
|
||||
MeshListenAutoPortEnd int `json:"mesh_listen_auto_port_end,omitempty"`
|
||||
FabricListenAddr string `json:"fabric_listen_addr,omitempty"`
|
||||
FabricListenPortMode string `json:"fabric_listen_port_mode,omitempty"`
|
||||
FabricListenAutoPortStart int `json:"fabric_listen_auto_port_start,omitempty"`
|
||||
FabricListenAutoPortEnd int `json:"fabric_listen_auto_port_end,omitempty"`
|
||||
MeshAdvertiseEndpoint string `json:"mesh_advertise_endpoint,omitempty"`
|
||||
MeshAdvertiseEndpointsJSON json.RawMessage `json:"mesh_advertise_endpoints_json,omitempty"`
|
||||
MeshAdvertiseTransport string `json:"mesh_advertise_transport,omitempty"`
|
||||
@@ -201,8 +192,7 @@ type DockerInstallProfile struct {
|
||||
type WindowsInstallProfile struct {
|
||||
SchemaVersion string `json:"schema_version"`
|
||||
ClusterID string `json:"cluster_id"`
|
||||
BackendURL string `json:"backend_url"`
|
||||
ControlPlaneEndpoints []string `json:"control_plane_endpoints,omitempty"`
|
||||
ClusterAuthorityPublicKey string `json:"cluster_authority_public_key,omitempty"`
|
||||
ArtifactEndpoints []string `json:"artifact_endpoints,omitempty"`
|
||||
FabricRegistryRecords json.RawMessage `json:"fabric_registry_records,omitempty"`
|
||||
NodeAgentArtifact *DockerArtifact `json:"node_agent_artifact,omitempty"`
|
||||
@@ -212,12 +202,12 @@ type WindowsInstallProfile struct {
|
||||
InstallDir string `json:"install_dir"`
|
||||
StartupMode string `json:"startup_mode"`
|
||||
WorkloadSupervisionEnabled bool `json:"workload_supervision_enabled"`
|
||||
MeshSyntheticRuntimeEnabled bool `json:"mesh_synthetic_runtime_enabled"`
|
||||
FabricRuntimeEnabled bool `json:"fabric_runtime_enabled"`
|
||||
MeshProductionForwardingEnabled bool `json:"mesh_production_forwarding_enabled"`
|
||||
MeshListenAddr string `json:"mesh_listen_addr,omitempty"`
|
||||
MeshListenPortMode string `json:"mesh_listen_port_mode,omitempty"`
|
||||
MeshListenAutoPortStart int `json:"mesh_listen_auto_port_start,omitempty"`
|
||||
MeshListenAutoPortEnd int `json:"mesh_listen_auto_port_end,omitempty"`
|
||||
FabricListenAddr string `json:"fabric_listen_addr,omitempty"`
|
||||
FabricListenPortMode string `json:"fabric_listen_port_mode,omitempty"`
|
||||
FabricListenAutoPortStart int `json:"fabric_listen_auto_port_start,omitempty"`
|
||||
FabricListenAutoPortEnd int `json:"fabric_listen_auto_port_end,omitempty"`
|
||||
MeshAdvertiseEndpoint string `json:"mesh_advertise_endpoint,omitempty"`
|
||||
MeshAdvertiseEndpointsJSON json.RawMessage `json:"mesh_advertise_endpoints_json,omitempty"`
|
||||
MeshAdvertiseTransport string `json:"mesh_advertise_transport,omitempty"`
|
||||
@@ -234,8 +224,7 @@ type WindowsInstallProfile struct {
|
||||
type LinuxInstallProfile struct {
|
||||
SchemaVersion string `json:"schema_version"`
|
||||
ClusterID string `json:"cluster_id"`
|
||||
BackendURL string `json:"backend_url"`
|
||||
ControlPlaneEndpoints []string `json:"control_plane_endpoints,omitempty"`
|
||||
ClusterAuthorityPublicKey string `json:"cluster_authority_public_key,omitempty"`
|
||||
ArtifactEndpoints []string `json:"artifact_endpoints,omitempty"`
|
||||
FabricRegistryRecords json.RawMessage `json:"fabric_registry_records,omitempty"`
|
||||
NodeAgentArtifact *DockerArtifact `json:"node_agent_artifact,omitempty"`
|
||||
@@ -245,12 +234,12 @@ type LinuxInstallProfile struct {
|
||||
InstallDir string `json:"install_dir"`
|
||||
StartupMode string `json:"startup_mode"`
|
||||
WorkloadSupervisionEnabled bool `json:"workload_supervision_enabled"`
|
||||
MeshSyntheticRuntimeEnabled bool `json:"mesh_synthetic_runtime_enabled"`
|
||||
FabricRuntimeEnabled bool `json:"fabric_runtime_enabled"`
|
||||
MeshProductionForwardingEnabled bool `json:"mesh_production_forwarding_enabled"`
|
||||
MeshListenAddr string `json:"mesh_listen_addr,omitempty"`
|
||||
MeshListenPortMode string `json:"mesh_listen_port_mode,omitempty"`
|
||||
MeshListenAutoPortStart int `json:"mesh_listen_auto_port_start,omitempty"`
|
||||
MeshListenAutoPortEnd int `json:"mesh_listen_auto_port_end,omitempty"`
|
||||
FabricListenAddr string `json:"fabric_listen_addr,omitempty"`
|
||||
FabricListenPortMode string `json:"fabric_listen_port_mode,omitempty"`
|
||||
FabricListenAutoPortStart int `json:"fabric_listen_auto_port_start,omitempty"`
|
||||
FabricListenAutoPortEnd int `json:"fabric_listen_auto_port_end,omitempty"`
|
||||
MeshAdvertiseEndpoint string `json:"mesh_advertise_endpoint,omitempty"`
|
||||
MeshAdvertiseEndpointsJSON json.RawMessage `json:"mesh_advertise_endpoints_json,omitempty"`
|
||||
MeshAdvertiseTransport string `json:"mesh_advertise_transport,omitempty"`
|
||||
@@ -264,6 +253,19 @@ type LinuxInstallProfile struct {
|
||||
Roles []string `json:"roles,omitempty"`
|
||||
}
|
||||
|
||||
type InstallJoinBundle struct {
|
||||
SchemaVersion string `json:"schema_version"`
|
||||
BundleKind string `json:"bundle_kind"`
|
||||
ClusterID string `json:"cluster_id"`
|
||||
ClusterAuthority *ClusterAuthorityDescriptor `json:"cluster_authority,omitempty"`
|
||||
AuthorityPayload json.RawMessage `json:"authority_payload,omitempty"`
|
||||
AuthoritySignature *ClusterSignature `json:"authority_signature,omitempty"`
|
||||
IssuedAt time.Time `json:"issued_at"`
|
||||
DockerInstallProfile *DockerInstallProfile `json:"docker_install_profile,omitempty"`
|
||||
WindowsInstallProfile *WindowsInstallProfile `json:"windows_install_profile,omitempty"`
|
||||
LinuxInstallProfile *LinuxInstallProfile `json:"linux_install_profile,omitempty"`
|
||||
}
|
||||
|
||||
type DockerArtifact struct {
|
||||
Kind string `json:"kind"`
|
||||
Image string `json:"image,omitempty"`
|
||||
@@ -324,15 +326,19 @@ type NodeUpdatePolicy struct {
|
||||
}
|
||||
|
||||
type NodeUpdateHint struct {
|
||||
SchemaVersion string `json:"schema_version"`
|
||||
Generation string `json:"generation,omitempty"`
|
||||
CheckNow bool `json:"check_now"`
|
||||
Products []string `json:"products,omitempty"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
DeliveryMode string `json:"delivery_mode,omitempty"`
|
||||
SubscriptionStatus string `json:"subscription_status,omitempty"`
|
||||
UpdateService *NodeUpdateServiceAssignment `json:"update_service,omitempty"`
|
||||
FallbackPollSeconds int `json:"fallback_poll_seconds,omitempty"`
|
||||
SchemaVersion string `json:"schema_version"`
|
||||
Generation string `json:"generation,omitempty"`
|
||||
CheckNow bool `json:"check_now"`
|
||||
Products []string `json:"products,omitempty"`
|
||||
TargetVersions map[string]string `json:"target_versions,omitempty"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
DeliveryMode string `json:"delivery_mode,omitempty"`
|
||||
SubscriptionStatus string `json:"subscription_status,omitempty"`
|
||||
UpdateService *NodeUpdateServiceAssignment `json:"update_service,omitempty"`
|
||||
UpdateServiceCandidates []NodeUpdateServiceAssignment `json:"update_service_candidates,omitempty"`
|
||||
RescuePollSeconds int `json:"rescue_poll_seconds,omitempty"`
|
||||
AuthorityPayload json.RawMessage `json:"authority_payload,omitempty"`
|
||||
AuthoritySignature *ClusterSignature `json:"authority_signature,omitempty"`
|
||||
}
|
||||
|
||||
type NodeUpdateServiceAssignment struct {
|
||||
@@ -356,23 +362,75 @@ type NodeUpdateServiceCandidate struct {
|
||||
}
|
||||
|
||||
type NodeUpdatePlan struct {
|
||||
SchemaVersion string `json:"schema_version"`
|
||||
ClusterID string `json:"cluster_id"`
|
||||
NodeID string `json:"node_id"`
|
||||
Product string `json:"product"`
|
||||
CurrentVersion string `json:"current_version,omitempty"`
|
||||
Action string `json:"action"`
|
||||
Reason string `json:"reason"`
|
||||
TargetVersion string `json:"target_version,omitempty"`
|
||||
Channel string `json:"channel,omitempty"`
|
||||
Strategy string `json:"strategy,omitempty"`
|
||||
RollbackAllowed bool `json:"rollback_allowed"`
|
||||
HealthWindowSec int `json:"health_window_seconds,omitempty"`
|
||||
Artifact *ReleaseArtifact `json:"artifact,omitempty"`
|
||||
AuthorityPayload json.RawMessage `json:"authority_payload,omitempty"`
|
||||
AuthoritySignature *ClusterSignature `json:"authority_signature,omitempty"`
|
||||
AuthorityQuorum *QuorumEnvelope `json:"authority_quorum,omitempty"`
|
||||
ProductionForwarding bool `json:"production_forwarding"`
|
||||
SchemaVersion string `json:"schema_version"`
|
||||
ClusterID string `json:"cluster_id"`
|
||||
NodeID string `json:"node_id"`
|
||||
Product string `json:"product"`
|
||||
CurrentVersion string `json:"current_version,omitempty"`
|
||||
Action string `json:"action"`
|
||||
Reason string `json:"reason"`
|
||||
TargetVersion string `json:"target_version,omitempty"`
|
||||
Channel string `json:"channel,omitempty"`
|
||||
Strategy string `json:"strategy,omitempty"`
|
||||
RollbackAllowed bool `json:"rollback_allowed"`
|
||||
HealthWindowSec int `json:"health_window_seconds,omitempty"`
|
||||
FabricRegistryRecords json.RawMessage `json:"fabric_registry_records,omitempty"`
|
||||
UpdateIntent *NodeUpdateIntent `json:"update_intent,omitempty"`
|
||||
RolloutLease *NodeUpdateLease `json:"rollout_lease,omitempty"`
|
||||
Artifact *ReleaseArtifact `json:"artifact,omitempty"`
|
||||
AuthorityPayload json.RawMessage `json:"authority_payload,omitempty"`
|
||||
AuthoritySignature *ClusterSignature `json:"authority_signature,omitempty"`
|
||||
AuthorityQuorum *QuorumEnvelope `json:"authority_quorum,omitempty"`
|
||||
ProductionForwarding bool `json:"production_forwarding"`
|
||||
}
|
||||
|
||||
type NodeUpdateArtifactContent struct {
|
||||
SchemaVersion string `json:"schema_version"`
|
||||
ArtifactID string `json:"artifact_id"`
|
||||
Product string `json:"product"`
|
||||
Version string `json:"version"`
|
||||
DataBase64 string `json:"data_base64"`
|
||||
SHA256 string `json:"sha256,omitempty"`
|
||||
ChunkSHA256 string `json:"chunk_sha256,omitempty"`
|
||||
SizeBytes int64 `json:"size_bytes,omitempty"`
|
||||
Offset int64 `json:"offset,omitempty"`
|
||||
ChunkSize int64 `json:"chunk_size,omitempty"`
|
||||
Complete bool `json:"complete,omitempty"`
|
||||
DistributorID string `json:"distributor_id,omitempty"`
|
||||
}
|
||||
|
||||
type NodeUpdateIntent struct {
|
||||
SchemaVersion string `json:"schema_version"`
|
||||
IntentID string `json:"intent_id"`
|
||||
ClusterID string `json:"cluster_id"`
|
||||
NodeID string `json:"node_id,omitempty"`
|
||||
Product string `json:"product"`
|
||||
TargetVersion string `json:"target_version"`
|
||||
Strategy string `json:"strategy"`
|
||||
Generation string `json:"generation"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
RollbackAllowed bool `json:"rollback_allowed"`
|
||||
HealthWindowSec int `json:"health_window_seconds,omitempty"`
|
||||
RequiredLease bool `json:"required_lease"`
|
||||
AllowedMirrors []string `json:"allowed_mirrors,omitempty"`
|
||||
}
|
||||
|
||||
type NodeUpdateLease struct {
|
||||
SchemaVersion string `json:"schema_version"`
|
||||
LeaseID string `json:"lease_id"`
|
||||
IntentID string `json:"intent_id"`
|
||||
ClusterID string `json:"cluster_id"`
|
||||
NodeID string `json:"node_id"`
|
||||
Product string `json:"product"`
|
||||
TargetVersion string `json:"target_version"`
|
||||
Strategy string `json:"strategy"`
|
||||
Status string `json:"status"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
MaxParallel int `json:"max_parallel"`
|
||||
ActiveUpdateCnt int `json:"active_update_count"`
|
||||
AcquiredAt time.Time `json:"acquired_at"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
}
|
||||
|
||||
type NodeBridgeReplayProductPlan struct {
|
||||
@@ -414,52 +472,87 @@ type NodeUpdateStatus struct {
|
||||
}
|
||||
|
||||
type StaleNodeRiskReport struct {
|
||||
ClusterID string `json:"cluster_id"`
|
||||
GeneratedAt time.Time `json:"generated_at"`
|
||||
HeartbeatStaleAfterSeconds int `json:"heartbeat_stale_after_seconds"`
|
||||
LegacyRemovalAllowed bool `json:"legacy_removal_allowed"`
|
||||
BridgeHoldRequired bool `json:"bridge_hold_required"`
|
||||
BridgeHoldNodeIDs []string `json:"bridge_hold_node_ids,omitempty"`
|
||||
BridgeHoldReasons []string `json:"bridge_hold_reasons,omitempty"`
|
||||
BlockedOperations []string `json:"blocked_operations,omitempty"`
|
||||
Nodes []StaleNodeRiskNode `json:"nodes"`
|
||||
Summary StaleNodeRiskSummary `json:"summary"`
|
||||
ClusterID string `json:"cluster_id"`
|
||||
GeneratedAt time.Time `json:"generated_at"`
|
||||
HeartbeatStaleAfterSeconds int `json:"heartbeat_stale_after_seconds"`
|
||||
FabricStandardCleanupAllowed bool `json:"fabric_standard_cleanup_allowed"`
|
||||
BridgeHoldRequired bool `json:"bridge_hold_required"`
|
||||
BridgeHoldNodeIDs []string `json:"bridge_hold_node_ids,omitempty"`
|
||||
BridgeHoldReasons []string `json:"bridge_hold_reasons,omitempty"`
|
||||
BlockedOperations []string `json:"blocked_operations,omitempty"`
|
||||
Nodes []StaleNodeRiskNode `json:"nodes"`
|
||||
Summary StaleNodeRiskSummary `json:"summary"`
|
||||
}
|
||||
|
||||
type StaleNodeRiskSummary struct {
|
||||
TotalNodes int `json:"total_nodes"`
|
||||
StaleNodes int `json:"stale_nodes"`
|
||||
BlockedNodes int `json:"blocked_nodes"`
|
||||
DirectPeerAlertNodes int `json:"direct_peer_alert_nodes"`
|
||||
ArtifactGapNodes int `json:"artifact_gap_nodes"`
|
||||
UnknownProfileNodes int `json:"unknown_profile_nodes"`
|
||||
WaitingUpdateStatusNodes int `json:"waiting_update_status_nodes"`
|
||||
UnknownVersionNodes int `json:"unknown_version_nodes"`
|
||||
LegacyRecoveryContractNodes int `json:"legacy_recovery_contract_nodes"`
|
||||
RecoveryBridgeRequiredNodes int `json:"recovery_bridge_required_nodes"`
|
||||
RecoveryBridgeReplayReadyNodes int `json:"recovery_bridge_replay_ready_nodes"`
|
||||
WaitingRecoveryHeartbeatNodes int `json:"waiting_recovery_heartbeat_nodes"`
|
||||
TotalNodes int `json:"total_nodes"`
|
||||
StaleNodes int `json:"stale_nodes"`
|
||||
BlockedNodes int `json:"blocked_nodes"`
|
||||
DirectPeerAlertNodes int `json:"direct_peer_alert_nodes"`
|
||||
AreaDiversityAlertNodes int `json:"area_diversity_alert_nodes"`
|
||||
IndependentIngressAlertNodes int `json:"independent_ingress_alert_nodes"`
|
||||
DirectoryDisseminationAlertNodes int `json:"directory_dissemination_alert_nodes"`
|
||||
UpdaterSubscriptionAlertNodes int `json:"updater_subscription_alert_nodes"`
|
||||
UpdaterWakeUnsupportedNodes int `json:"updater_wake_unsupported_nodes"`
|
||||
UpdaterRuntimeMissingNodes int `json:"updater_runtime_missing_nodes"`
|
||||
StandardUpdaterLineNodes int `json:"standard_updater_line_nodes"`
|
||||
StagedSelfUpdatePendingNodes int `json:"staged_self_update_pending_nodes"`
|
||||
PostUpdateHeartbeatGapNodes int `json:"post_update_heartbeat_gap_nodes"`
|
||||
ArtifactGapNodes int `json:"artifact_gap_nodes"`
|
||||
StandardControlDependencyNodes int `json:"standard_control_dependency_nodes"`
|
||||
RegistryCandidateOnlyNodes int `json:"registry_candidate_only_nodes"`
|
||||
RegistryJoinContractMissingNodes int `json:"registry_join_missing_nodes"`
|
||||
UnknownProfileNodes int `json:"unknown_profile_nodes"`
|
||||
WaitingUpdateStatusNodes int `json:"waiting_update_status_nodes"`
|
||||
UnknownVersionNodes int `json:"unknown_version_nodes"`
|
||||
StandardRecoveryContractNodes int `json:"standard_recovery_contract_nodes"`
|
||||
RecoveryBridgeRequiredNodes int `json:"recovery_bridge_required_nodes"`
|
||||
RecoveryBridgeReplayReadyNodes int `json:"recovery_bridge_replay_ready_nodes"`
|
||||
WaitingRecoveryHeartbeatNodes int `json:"waiting_recovery_heartbeat_nodes"`
|
||||
}
|
||||
|
||||
type StaleNodeRiskNode struct {
|
||||
NodeID string `json:"node_id"`
|
||||
Name string `json:"name"`
|
||||
RegistrationStatus string `json:"registration_status"`
|
||||
HealthStatus string `json:"health_status"`
|
||||
ReportedVersion *string `json:"reported_version,omitempty"`
|
||||
LastSeenAt *time.Time `json:"last_seen_at,omitempty"`
|
||||
HeartbeatStale bool `json:"heartbeat_stale"`
|
||||
Blocked bool `json:"blocked"`
|
||||
DirectPeerAlert bool `json:"direct_peer_alert"`
|
||||
DirectPeerReadyCount int `json:"direct_peer_ready_count,omitempty"`
|
||||
DirectPeerTargetCount int `json:"direct_peer_target_count,omitempty"`
|
||||
DirectPeerDeficit int `json:"direct_peer_deficit,omitempty"`
|
||||
Alerts []string `json:"alerts,omitempty"`
|
||||
RecoveryBridgeRequired bool `json:"recovery_bridge_required"`
|
||||
RecoveryBridgeReplayReady bool `json:"recovery_bridge_replay_ready"`
|
||||
RecoveryBridgeActions []string `json:"recovery_bridge_actions,omitempty"`
|
||||
Risks []string `json:"risks,omitempty"`
|
||||
Products []StaleNodeRiskProduct `json:"products,omitempty"`
|
||||
NodeID string `json:"node_id"`
|
||||
Name string `json:"name"`
|
||||
Area string `json:"area,omitempty"`
|
||||
RegistrationStatus string `json:"registration_status"`
|
||||
HealthStatus string `json:"health_status"`
|
||||
ReportedVersion *string `json:"reported_version,omitempty"`
|
||||
LastSeenAt *time.Time `json:"last_seen_at,omitempty"`
|
||||
HeartbeatStale bool `json:"heartbeat_stale"`
|
||||
Blocked bool `json:"blocked"`
|
||||
DirectPeerAlert bool `json:"direct_peer_alert"`
|
||||
DirectPeerReadyCount int `json:"direct_peer_ready_count,omitempty"`
|
||||
DirectPeerTargetCount int `json:"direct_peer_target_count,omitempty"`
|
||||
DirectPeerDeficit int `json:"direct_peer_deficit,omitempty"`
|
||||
DirectReadyAreas []string `json:"direct_ready_areas,omitempty"`
|
||||
ExternalAreaReadyCount int `json:"external_area_ready_count,omitempty"`
|
||||
RequiredExternalAreaCount int `json:"required_external_area_count,omitempty"`
|
||||
AreaDiversityAlert bool `json:"area_diversity_alert"`
|
||||
RequiredIndependentIngressCount int `json:"required_independent_ingress_count,omitempty"`
|
||||
IndependentIngressAlert bool `json:"independent_ingress_alert"`
|
||||
FullDirectoryExpected bool `json:"full_directory_expected"`
|
||||
KnownPeerDirectoryCount int `json:"known_peer_directory_count,omitempty"`
|
||||
ExpectedDirectoryCount int `json:"expected_directory_count,omitempty"`
|
||||
DirectoryDisseminationAlert bool `json:"directory_dissemination_alert"`
|
||||
UpdaterSubscriptionAlert bool `json:"updater_subscription_alert"`
|
||||
UpdaterWakeUnsupported bool `json:"updater_wake_unsupported"`
|
||||
UpdaterRuntimeMissing bool `json:"updater_runtime_missing"`
|
||||
StandardUpdaterLine bool `json:"standard_updater_line"`
|
||||
StagedSelfUpdatePending bool `json:"staged_self_update_pending"`
|
||||
PostUpdateHeartbeatGap bool `json:"post_update_heartbeat_gap"`
|
||||
StandardControlDependency bool `json:"standard_control_dependency"`
|
||||
StandardControlURL string `json:"standard_control_url,omitempty"`
|
||||
RegistryRuntimeStatus string `json:"registry_runtime_status,omitempty"`
|
||||
RegistryJoinContractMissing bool `json:"registry_join_missing"`
|
||||
ResolvedServiceCount int `json:"resolved_service_count,omitempty"`
|
||||
IndependentIngressCount int `json:"independent_ingress_count,omitempty"`
|
||||
Alerts []string `json:"alerts,omitempty"`
|
||||
RecoveryBridgeRequired bool `json:"recovery_bridge_required"`
|
||||
RecoveryBridgeReplayReady bool `json:"recovery_bridge_replay_ready"`
|
||||
RecoveryBridgeActions []string `json:"recovery_bridge_actions,omitempty"`
|
||||
Risks []string `json:"risks,omitempty"`
|
||||
Products []StaleNodeRiskProduct `json:"products,omitempty"`
|
||||
}
|
||||
|
||||
type StaleNodeRiskProduct struct {
|
||||
@@ -478,13 +571,15 @@ type StaleNodeRiskProduct struct {
|
||||
LastStatusPhase string `json:"last_status_phase,omitempty"`
|
||||
LastStatusValue string `json:"last_status_value,omitempty"`
|
||||
LastStatusReason string `json:"last_status_reason,omitempty"`
|
||||
StagedSelfUpdatePending bool `json:"staged_self_update_pending"`
|
||||
PostUpdateHeartbeatGap bool `json:"post_update_heartbeat_gap"`
|
||||
RecoveryBridgeRequired bool `json:"recovery_bridge_required"`
|
||||
RecoveryBridgeReplayReady bool `json:"recovery_bridge_replay_ready"`
|
||||
RecoveryBridgeMode string `json:"recovery_bridge_mode,omitempty"`
|
||||
Risks []string `json:"risks,omitempty"`
|
||||
}
|
||||
|
||||
type NodeBootstrap struct {
|
||||
type NodeJoinContract struct {
|
||||
NodeID string `json:"node_id"`
|
||||
ClusterID string `json:"cluster_id"`
|
||||
IdentityStatus string `json:"identity_status"`
|
||||
@@ -664,23 +759,23 @@ type FabricServiceChannelAdaptivePolicy struct {
|
||||
}
|
||||
|
||||
type FabricServiceChannelPoolPolicy struct {
|
||||
SchemaVersion string `json:"schema_version"`
|
||||
Fingerprint string `json:"fingerprint,omitempty"`
|
||||
EntryPoolNodeIDs []string `json:"entry_pool_node_ids,omitempty"`
|
||||
ExitPoolNodeIDs []string `json:"exit_pool_node_ids,omitempty"`
|
||||
PreferredEntryNodeID string `json:"preferred_entry_node_id,omitempty"`
|
||||
PreferredExitNodeID string `json:"preferred_exit_node_id,omitempty"`
|
||||
SelectionStrategy string `json:"selection_strategy"`
|
||||
RouteRebuild string `json:"route_rebuild"`
|
||||
EntryFailover string `json:"entry_failover"`
|
||||
ExitFailover string `json:"exit_failover"`
|
||||
BackendFallbackAllowed bool `json:"backend_fallback_allowed"`
|
||||
StickySession bool `json:"sticky_session"`
|
||||
Source string `json:"source"`
|
||||
UpdatedByUserID *string `json:"updated_by_user_id,omitempty"`
|
||||
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
||||
ControlPlaneOnly bool `json:"control_plane_only"`
|
||||
ProductionForwarding bool `json:"production_forwarding"`
|
||||
SchemaVersion string `json:"schema_version"`
|
||||
Fingerprint string `json:"fingerprint,omitempty"`
|
||||
EntryPoolNodeIDs []string `json:"entry_pool_node_ids,omitempty"`
|
||||
ExitPoolNodeIDs []string `json:"exit_pool_node_ids,omitempty"`
|
||||
PreferredEntryNodeID string `json:"preferred_entry_node_id,omitempty"`
|
||||
PreferredExitNodeID string `json:"preferred_exit_node_id,omitempty"`
|
||||
SelectionStrategy string `json:"selection_strategy"`
|
||||
RouteRebuild string `json:"route_rebuild"`
|
||||
EntryFailover string `json:"entry_failover"`
|
||||
ExitFailover string `json:"exit_failover"`
|
||||
CompatFallbackAllowed bool `json:"degraded_route_allowed"`
|
||||
StickySession bool `json:"sticky_session"`
|
||||
Source string `json:"source"`
|
||||
UpdatedByUserID *string `json:"updated_by_user_id,omitempty"`
|
||||
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
||||
ControlPlaneOnly bool `json:"control_plane_only"`
|
||||
ProductionForwarding bool `json:"production_forwarding"`
|
||||
}
|
||||
|
||||
type FabricServiceChannelBreadcrumbWindowPolicy struct {
|
||||
@@ -851,12 +946,12 @@ type NodeSyntheticMeshConfig struct {
|
||||
ServiceChannelFeedback *FabricServiceChannelRouteFeedbackReport `json:"service_channel_route_feedback,omitempty"`
|
||||
ServiceChannelAdaptivePolicy *FabricServiceChannelAdaptivePolicy `json:"service_channel_adaptive_policy,omitempty"`
|
||||
ServiceChannelRemediationCommands []FabricServiceChannelAccessRemediationCommand `json:"service_channel_remediation_commands,omitempty"`
|
||||
MeshListener *NodeMeshListenerConfig `json:"mesh_listener,omitempty"`
|
||||
FabricListener *NodeFabricListenerConfig `json:"fabric_listener,omitempty"`
|
||||
Routes []SyntheticMeshRouteConfig `json:"routes"`
|
||||
ProductionForwarding bool `json:"production_forwarding"`
|
||||
}
|
||||
|
||||
type NodeMeshListenerConfig struct {
|
||||
type NodeFabricListenerConfig struct {
|
||||
SchemaVersion string `json:"schema_version"`
|
||||
Source string `json:"source"`
|
||||
DesiredState string `json:"desired_state"`
|
||||
@@ -1094,23 +1189,23 @@ type FabricServiceChannelLeaseRecord struct {
|
||||
}
|
||||
|
||||
type FabricServiceChannelLeaseSummary struct {
|
||||
ClusterID string `json:"cluster_id"`
|
||||
ChannelID string `json:"channel_id"`
|
||||
ResourceID string `json:"resource_id,omitempty"`
|
||||
ServiceClass string `json:"service_class"`
|
||||
Status string `json:"status"`
|
||||
SelectedEntryNodeID string `json:"selected_entry_node_id,omitempty"`
|
||||
SelectedExitNodeID string `json:"selected_exit_node_id,omitempty"`
|
||||
AllowedChannels []string `json:"allowed_channels,omitempty"`
|
||||
PrimaryRouteID string `json:"primary_route_id,omitempty"`
|
||||
PrimaryRouteStatus string `json:"primary_route_status,omitempty"`
|
||||
DataPlane FabricServiceChannelDataPlaneContract `json:"data_plane,omitempty"`
|
||||
ForceBackendFallback bool `json:"force_backend_fallback"`
|
||||
Expired bool `json:"expired"`
|
||||
IssuedAt time.Time `json:"issued_at"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ClusterID string `json:"cluster_id"`
|
||||
ChannelID string `json:"channel_id"`
|
||||
ResourceID string `json:"resource_id,omitempty"`
|
||||
ServiceClass string `json:"service_class"`
|
||||
Status string `json:"status"`
|
||||
SelectedEntryNodeID string `json:"selected_entry_node_id,omitempty"`
|
||||
SelectedExitNodeID string `json:"selected_exit_node_id,omitempty"`
|
||||
AllowedChannels []string `json:"allowed_channels,omitempty"`
|
||||
PrimaryRouteID string `json:"primary_route_id,omitempty"`
|
||||
PrimaryRouteStatus string `json:"primary_route_status,omitempty"`
|
||||
DataPlane FabricServiceChannelDataPlaneContract `json:"data_plane,omitempty"`
|
||||
ForceCompatFallback bool `json:"force_degraded_route"`
|
||||
Expired bool `json:"expired"`
|
||||
IssuedAt time.Time `json:"issued_at"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type FabricServiceChannelLeaseMaintenance struct {
|
||||
@@ -1139,15 +1234,15 @@ type FabricServiceChannelAccessTelemetry struct {
|
||||
TotalAccepted int `json:"total_accepted"`
|
||||
SignedAccepted int `json:"signed_accepted"`
|
||||
IntrospectionAccepted int `json:"introspection_accepted"`
|
||||
LegacyUnsignedAccepted int `json:"legacy_unsigned_accepted"`
|
||||
BackendFallbackCount int `json:"backend_fallback_count"`
|
||||
BackendFallbackBlockedCount int `json:"backend_fallback_blocked_count,omitempty"`
|
||||
CompatUnsignedAccepted int `json:"unsigned_accepted"`
|
||||
CompatFallbackCount int `json:"degraded_route_use_count"`
|
||||
CompatFallbackBlockedCount int `json:"degraded_route_blocked_count,omitempty"`
|
||||
FabricRouteSendFailureCount int `json:"fabric_route_send_failure_count,omitempty"`
|
||||
DataPlaneContractCount int `json:"data_plane_contract_count,omitempty"`
|
||||
LastDataPlaneMode string `json:"last_data_plane_mode,omitempty"`
|
||||
LastWorkingDataTransport string `json:"last_working_data_transport,omitempty"`
|
||||
LastSteadyStateTransport string `json:"last_steady_state_transport,omitempty"`
|
||||
LastBackendRelayPolicy string `json:"last_backend_relay_policy,omitempty"`
|
||||
LastCompatRelayPolicy string `json:"last_degraded_route_policy,omitempty"`
|
||||
LastLogicalFlowMode string `json:"last_logical_flow_mode,omitempty"`
|
||||
LastDataPlaneViolationStatus string `json:"last_data_plane_violation_status,omitempty"`
|
||||
LastDataPlaneViolationReason string `json:"last_data_plane_violation_reason,omitempty"`
|
||||
@@ -1184,15 +1279,15 @@ type FabricServiceChannelAccessTelemetryNode struct {
|
||||
TotalAccepted int `json:"total_accepted"`
|
||||
SignedAccepted int `json:"signed_accepted"`
|
||||
IntrospectionAccepted int `json:"introspection_accepted"`
|
||||
LegacyUnsignedAccepted int `json:"legacy_unsigned_accepted"`
|
||||
BackendFallbackCount int `json:"backend_fallback_count"`
|
||||
BackendFallbackBlockedCount int `json:"backend_fallback_blocked_count,omitempty"`
|
||||
CompatUnsignedAccepted int `json:"unsigned_accepted"`
|
||||
CompatFallbackCount int `json:"degraded_route_use_count"`
|
||||
CompatFallbackBlockedCount int `json:"degraded_route_blocked_count,omitempty"`
|
||||
FabricRouteSendFailureCount int `json:"fabric_route_send_failure_count,omitempty"`
|
||||
DataPlaneContractCount int `json:"data_plane_contract_count,omitempty"`
|
||||
LastDataPlaneMode string `json:"last_data_plane_mode,omitempty"`
|
||||
LastWorkingDataTransport string `json:"last_working_data_transport,omitempty"`
|
||||
LastSteadyStateTransport string `json:"last_steady_state_transport,omitempty"`
|
||||
LastBackendRelayPolicy string `json:"last_backend_relay_policy,omitempty"`
|
||||
LastCompatRelayPolicy string `json:"last_degraded_route_policy,omitempty"`
|
||||
LastLogicalFlowMode string `json:"last_logical_flow_mode,omitempty"`
|
||||
LastDataPlaneViolationStatus string `json:"last_data_plane_violation_status,omitempty"`
|
||||
LastDataPlaneViolationReason string `json:"last_data_plane_violation_reason,omitempty"`
|
||||
@@ -1219,17 +1314,17 @@ type FabricServiceChannelAccessTelemetryChannel struct {
|
||||
SelectedExitNodeID string `json:"selected_exit_node_id,omitempty"`
|
||||
PrimaryRouteID string `json:"primary_route_id,omitempty"`
|
||||
PrimaryRouteStatus string `json:"primary_route_status,omitempty"`
|
||||
ForceBackendFallback bool `json:"force_backend_fallback"`
|
||||
ForceCompatFallback bool `json:"force_degraded_route"`
|
||||
EntryNodeTotalAccepted int `json:"entry_node_total_accepted"`
|
||||
EntryNodeIntrospectionAccepted int `json:"entry_node_introspection_accepted"`
|
||||
EntryNodeBackendFallbackCount int `json:"entry_node_backend_fallback_count"`
|
||||
EntryNodeBackendFallbackBlockedCount int `json:"entry_node_backend_fallback_blocked_count,omitempty"`
|
||||
EntryNodeCompatFallbackCount int `json:"entry_node_degraded_route_count"`
|
||||
EntryNodeCompatFallbackBlockedCount int `json:"entry_node_degraded_route_blocked_count,omitempty"`
|
||||
EntryNodeFabricRouteSendFailureCount int `json:"entry_node_fabric_route_send_failure_count,omitempty"`
|
||||
EntryNodeDataPlaneContractCount int `json:"entry_node_data_plane_contract_count,omitempty"`
|
||||
EntryNodeLastDataPlaneMode string `json:"entry_node_last_data_plane_mode,omitempty"`
|
||||
EntryNodeLastWorkingDataTransport string `json:"entry_node_last_working_data_transport,omitempty"`
|
||||
EntryNodeLastSteadyStateTransport string `json:"entry_node_last_steady_state_transport,omitempty"`
|
||||
EntryNodeLastBackendRelayPolicy string `json:"entry_node_last_backend_relay_policy,omitempty"`
|
||||
EntryNodeLastCompatRelayPolicy string `json:"entry_node_last_degraded_route_policy,omitempty"`
|
||||
EntryNodeLastLogicalFlowMode string `json:"entry_node_last_logical_flow_mode,omitempty"`
|
||||
EntryNodeLastDataPlaneViolationStatus string `json:"entry_node_last_data_plane_violation_status,omitempty"`
|
||||
EntryNodeLastDataPlaneViolationReason string `json:"entry_node_last_data_plane_violation_reason,omitempty"`
|
||||
@@ -1305,26 +1400,26 @@ type FabricServiceChannelAccessRemediationCommand struct {
|
||||
}
|
||||
|
||||
type FabricServiceChannelLeaseIntrospection struct {
|
||||
SchemaVersion string `json:"schema_version"`
|
||||
ClusterID string `json:"cluster_id"`
|
||||
ChannelID string `json:"channel_id"`
|
||||
ResourceID string `json:"resource_id,omitempty"`
|
||||
ServiceClass string `json:"service_class"`
|
||||
Allowed bool `json:"allowed"`
|
||||
Status string `json:"status"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
AcceptedBy string `json:"accepted_by"`
|
||||
SelectedEntryNodeID string `json:"selected_entry_node_id,omitempty"`
|
||||
SelectedExitNodeID string `json:"selected_exit_node_id,omitempty"`
|
||||
AllowedChannels []string `json:"allowed_channels,omitempty"`
|
||||
PreferredRouteID string `json:"preferred_route_id,omitempty"`
|
||||
ForceBackendFallback bool `json:"force_backend_fallback"`
|
||||
LeaseStatus string `json:"lease_status,omitempty"`
|
||||
PrimaryRoute FabricServiceChannelRoute `json:"primary_route,omitempty"`
|
||||
DataPlane FabricServiceChannelDataPlaneContract `json:"data_plane,omitempty"`
|
||||
RouteGeneration string `json:"route_generation,omitempty"`
|
||||
FencingEpoch int64 `json:"fencing_epoch,omitempty"`
|
||||
ExpiresAt time.Time `json:"expires_at,omitempty"`
|
||||
SchemaVersion string `json:"schema_version"`
|
||||
ClusterID string `json:"cluster_id"`
|
||||
ChannelID string `json:"channel_id"`
|
||||
ResourceID string `json:"resource_id,omitempty"`
|
||||
ServiceClass string `json:"service_class"`
|
||||
Allowed bool `json:"allowed"`
|
||||
Status string `json:"status"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
AcceptedBy string `json:"accepted_by"`
|
||||
SelectedEntryNodeID string `json:"selected_entry_node_id,omitempty"`
|
||||
SelectedExitNodeID string `json:"selected_exit_node_id,omitempty"`
|
||||
AllowedChannels []string `json:"allowed_channels,omitempty"`
|
||||
PreferredRouteID string `json:"preferred_route_id,omitempty"`
|
||||
ForceCompatFallback bool `json:"force_degraded_route"`
|
||||
LeaseStatus string `json:"lease_status,omitempty"`
|
||||
PrimaryRoute FabricServiceChannelRoute `json:"primary_route,omitempty"`
|
||||
DataPlane FabricServiceChannelDataPlaneContract `json:"data_plane,omitempty"`
|
||||
RouteGeneration string `json:"route_generation,omitempty"`
|
||||
FencingEpoch int64 `json:"fencing_epoch,omitempty"`
|
||||
ExpiresAt time.Time `json:"expires_at,omitempty"`
|
||||
}
|
||||
|
||||
type FabricServiceChannelRouteFeedbackObservation struct {
|
||||
@@ -1970,7 +2065,17 @@ type CreateJoinRequestInput struct {
|
||||
RequestedRoles json.RawMessage
|
||||
}
|
||||
|
||||
type GetJoinRequestBootstrapInput struct {
|
||||
type RegisterFabricNodeInput struct {
|
||||
ClusterID string
|
||||
NodeKey string
|
||||
Name string
|
||||
OwnershipType string
|
||||
OwnerOrganizationID *string
|
||||
ReportedVersion *string
|
||||
Metadata json.RawMessage
|
||||
}
|
||||
|
||||
type GetJoinRequestJoinInput struct {
|
||||
ClusterID string
|
||||
JoinRequestID string
|
||||
NodeFingerprint string
|
||||
@@ -1988,14 +2093,14 @@ type ApproveJoinRequestInput struct {
|
||||
}
|
||||
|
||||
type ApprovedJoinRequest struct {
|
||||
JoinRequest NodeJoinRequest `json:"join_request"`
|
||||
Bootstrap NodeBootstrap `json:"node_bootstrap"`
|
||||
JoinRequest NodeJoinRequest `json:"join_request"`
|
||||
JoinContract NodeJoinContract `json:"node_join"`
|
||||
}
|
||||
|
||||
type JoinRequestBootstrapResult struct {
|
||||
Status string `json:"status"`
|
||||
JoinRequest NodeJoinRequest `json:"join_request"`
|
||||
Bootstrap *NodeBootstrap `json:"node_bootstrap,omitempty"`
|
||||
type JoinRequestJoinResult struct {
|
||||
Status string `json:"status"`
|
||||
JoinRequest NodeJoinRequest `json:"join_request"`
|
||||
JoinContract *NodeJoinContract `json:"node_join,omitempty"`
|
||||
}
|
||||
|
||||
type RejectJoinRequestInput struct {
|
||||
@@ -2114,15 +2219,24 @@ type UpsertNodeUpdatePolicyInput struct {
|
||||
}
|
||||
|
||||
type GetNodeUpdatePlanInput struct {
|
||||
ClusterID string
|
||||
NodeID string
|
||||
Product string
|
||||
CurrentVersion string
|
||||
OS string
|
||||
Arch string
|
||||
InstallType string
|
||||
Channel string
|
||||
ArtifactOrigin string
|
||||
ClusterID string
|
||||
NodeID string
|
||||
Product string
|
||||
CurrentVersion string
|
||||
OS string
|
||||
Arch string
|
||||
InstallType string
|
||||
Channel string
|
||||
ArtifactOrigin string
|
||||
ExecutorCapabilities []string
|
||||
}
|
||||
|
||||
type GetNodeUpdateArtifactContentInput struct {
|
||||
ClusterID string
|
||||
NodeID string
|
||||
ArtifactID string
|
||||
Offset int64
|
||||
Length int64
|
||||
}
|
||||
|
||||
type GetStaleNodeRiskReportInput struct {
|
||||
@@ -2276,38 +2390,38 @@ type SetFabricEgressPoolNodeInput struct {
|
||||
}
|
||||
|
||||
type IssueFabricServiceChannelLeaseInput struct {
|
||||
ActorUserID string
|
||||
ClusterID string
|
||||
OrganizationID string
|
||||
UserID string
|
||||
ResourceID string
|
||||
ServiceClass string
|
||||
EntryNodeIDs []string
|
||||
ExitNodeIDs []string
|
||||
PreferredEntryNodeID string
|
||||
PreferredExitNodeID string
|
||||
RequiredRoles []string
|
||||
AllowedChannels []string
|
||||
QoS json.RawMessage
|
||||
Failover json.RawMessage
|
||||
Metadata json.RawMessage
|
||||
TTL time.Duration
|
||||
BackendFallbackAllowed *bool
|
||||
ActorUserID string
|
||||
ClusterID string
|
||||
OrganizationID string
|
||||
UserID string
|
||||
ResourceID string
|
||||
ServiceClass string
|
||||
EntryNodeIDs []string
|
||||
ExitNodeIDs []string
|
||||
PreferredEntryNodeID string
|
||||
PreferredExitNodeID string
|
||||
RequiredRoles []string
|
||||
AllowedChannels []string
|
||||
QoS json.RawMessage
|
||||
Failover json.RawMessage
|
||||
Metadata json.RawMessage
|
||||
TTL time.Duration
|
||||
CompatFallbackAllowed *bool
|
||||
}
|
||||
|
||||
type UpdateFabricServiceChannelPoolPolicyInput struct {
|
||||
ActorUserID string
|
||||
ClusterID string
|
||||
EntryPoolNodeIDs []string
|
||||
ExitPoolNodeIDs []string
|
||||
PreferredEntryNodeID string
|
||||
PreferredExitNodeID string
|
||||
SelectionStrategy string
|
||||
RouteRebuild string
|
||||
EntryFailover string
|
||||
ExitFailover string
|
||||
BackendFallbackAllowed *bool
|
||||
StickySession *bool
|
||||
ActorUserID string
|
||||
ClusterID string
|
||||
EntryPoolNodeIDs []string
|
||||
ExitPoolNodeIDs []string
|
||||
PreferredEntryNodeID string
|
||||
PreferredExitNodeID string
|
||||
SelectionStrategy string
|
||||
RouteRebuild string
|
||||
EntryFailover string
|
||||
ExitFailover string
|
||||
CompatFallbackAllowed *bool
|
||||
StickySession *bool
|
||||
}
|
||||
|
||||
type UpdateFabricServiceChannelBreadcrumbWindowPolicyInput struct {
|
||||
|
||||
@@ -60,13 +60,30 @@ func (m *Module) Name() string {
|
||||
}
|
||||
|
||||
func (m *Module) RegisterRoutes(router chi.Router) {
|
||||
router.Get("/ui/admin", m.renderAdminHTML)
|
||||
router.Get("/ui/htmx-lite.js", m.renderHTMXLiteJS)
|
||||
router.Get("/downloads/{fileName}", m.downloadReleaseFile)
|
||||
router.Get("/downloads/releases/{version}/{fileName}", m.downloadVersionedReleaseFile)
|
||||
router.Route("/clusters", func(r chi.Router) {
|
||||
r.Get("/", m.listClusters)
|
||||
r.Post("/", m.createCluster)
|
||||
r.Get("/{clusterID}", m.getCluster)
|
||||
r.Put("/{clusterID}", m.updateCluster)
|
||||
r.Get("/{clusterID}/nodes", m.listClusterNodes)
|
||||
r.Get("/{clusterID}/ui/overview", m.renderFarmOverviewHTML)
|
||||
r.Get("/{clusterID}/ui/nodes", m.renderNodesHTML)
|
||||
r.Get("/{clusterID}/ui/nodes/fragment", m.renderNodesHTMLFragment)
|
||||
r.Get("/{clusterID}/ui/nodes/{nodeID}/details", m.renderNodeDetailsHTML)
|
||||
r.Get("/{clusterID}/ui/updates", m.renderUpdatesHTML)
|
||||
r.Get("/{clusterID}/ui/updates/fragment", m.renderUpdatesHTMLFragment)
|
||||
r.Post("/{clusterID}/ui/updates/check-now", m.renderUpdateCheckNowAllHTML)
|
||||
r.Post("/{clusterID}/ui/updates/{nodeID}/check-now", m.renderUpdateCheckNowHTML)
|
||||
r.Get("/{clusterID}/ui/topology", m.renderTopologyHTML)
|
||||
r.Get("/{clusterID}/ui/fabric", m.renderFabricConsoleHTML)
|
||||
r.Post("/{clusterID}/ui/fabric/policy", m.renderFabricPolicyHTML)
|
||||
r.Get("/{clusterID}/ui/web-control", m.renderWebControlHTML)
|
||||
r.Post("/{clusterID}/ui/web-control/desired", m.renderWebControlDesiredHTML)
|
||||
r.Get("/{clusterID}/ui/audit", m.renderAuditConsoleHTML)
|
||||
r.Get("/{clusterID}/node-groups", m.listNodeGroups)
|
||||
r.Post("/{clusterID}/node-groups", m.createNodeGroup)
|
||||
r.Get("/{clusterID}/join-requests", m.listJoinRequests)
|
||||
@@ -84,6 +101,7 @@ func (m *Module) RegisterRoutes(router chi.Router) {
|
||||
r.Post("/{clusterID}/updates/releases", m.createReleaseVersion)
|
||||
r.Put("/{clusterID}/nodes/{nodeID}/updates/policy", m.upsertNodeUpdatePolicy)
|
||||
r.Get("/{clusterID}/nodes/{nodeID}/updates/plan", m.getNodeUpdatePlan)
|
||||
r.Get("/{clusterID}/nodes/{nodeID}/updates/artifacts/{artifactID}/content", m.getNodeUpdateArtifactContent)
|
||||
r.Get("/{clusterID}/nodes/{nodeID}/updates/bridge-replay-plan", m.getNodeBridgeReplayPlan)
|
||||
r.Post("/{clusterID}/nodes/{nodeID}/updates/status", m.reportNodeUpdateStatus)
|
||||
r.Get("/{clusterID}/nodes/{nodeID}/updates/statuses", m.listNodeUpdateStatuses)
|
||||
@@ -191,9 +209,45 @@ func (m *Module) downloadReleaseFile(w http.ResponseWriter, r *http.Request) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
setReleaseDownloadHeaders(w, fileName)
|
||||
http.ServeFile(w, r, path)
|
||||
}
|
||||
|
||||
func (m *Module) downloadVersionedReleaseFile(w http.ResponseWriter, r *http.Request) {
|
||||
version := filepath.Base(strings.TrimSpace(chi.URLParam(r, "version")))
|
||||
fileName := filepath.Base(strings.TrimSpace(chi.URLParam(r, "fileName")))
|
||||
if version == "" || version == "." || version != strings.TrimSpace(chi.URLParam(r, "version")) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
if fileName == "" || fileName == "." || fileName != strings.TrimSpace(chi.URLParam(r, "fileName")) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
releaseDir := strings.TrimSpace(os.Getenv("RAP_RELEASE_DIR"))
|
||||
if releaseDir == "" {
|
||||
releaseDir = "/tmp/rap-release"
|
||||
}
|
||||
path := filepath.Join(releaseDir, "releases", version, fileName)
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
setReleaseDownloadHeaders(w, fileName)
|
||||
http.ServeFile(w, r, path)
|
||||
}
|
||||
|
||||
func setReleaseDownloadHeaders(w http.ResponseWriter, fileName string) {
|
||||
switch strings.ToLower(filepath.Ext(fileName)) {
|
||||
case ".apk":
|
||||
w.Header().Set("Content-Type", "application/vnd.android.package-archive")
|
||||
w.Header().Set("Content-Disposition", `attachment; filename="`+fileName+`"`)
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
case ".json":
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Module) listClusters(w http.ResponseWriter, r *http.Request) {
|
||||
items, err := m.service.ListClusters(r.Context(), r.URL.Query().Get("actor_user_id"))
|
||||
if writeServiceError(w, err) {
|
||||
@@ -340,14 +394,10 @@ func adminRuntimeProjectionResponse(statusCode int, status string, reason string
|
||||
|
||||
func isAllowedAdminRuntimeProjectionScope(scope string, serviceClass string) bool {
|
||||
switch serviceClass {
|
||||
case FabricServiceClassPlatformAdmin:
|
||||
return scope == "platform"
|
||||
case FabricServiceClassClusterAdmin:
|
||||
return scope == "cluster"
|
||||
case FabricServiceClassOrganization:
|
||||
return scope == "organization"
|
||||
case FabricServiceClassUserPortal:
|
||||
return scope == "user" || scope == "organization"
|
||||
case FabricServiceClassAdminIngress:
|
||||
return scope == "platform" || scope == "cluster"
|
||||
case FabricServiceClassPublicIngress:
|
||||
return scope == "organization" || scope == "user"
|
||||
default:
|
||||
return false
|
||||
}
|
||||
@@ -357,18 +407,22 @@ func adminRuntimeManifest(scope string, serviceClass string) map[string]any {
|
||||
sections := []string{"status"}
|
||||
actions := []string{"read_status"}
|
||||
switch strings.TrimSpace(serviceClass) {
|
||||
case FabricServiceClassPlatformAdmin:
|
||||
case FabricServiceClassAdminIngress:
|
||||
sections = []string{"clusters", "nodes", "roles", "fabric", "workloads", "audit"}
|
||||
actions = []string{"read_platform_summary", "read_cluster_summaries", "read_node_status"}
|
||||
case FabricServiceClassClusterAdmin:
|
||||
sections = []string{"cluster", "nodes", "fabric", "workloads", "audit"}
|
||||
actions = []string{"read_cluster_summary", "read_node_status"}
|
||||
case FabricServiceClassOrganization:
|
||||
if scope == "cluster" {
|
||||
sections = []string{"cluster", "nodes", "fabric", "workloads", "audit"}
|
||||
actions = []string{"read_cluster_summary", "read_node_status"}
|
||||
} else {
|
||||
actions = []string{"read_platform_summary", "read_cluster_summaries", "read_node_status"}
|
||||
}
|
||||
case FabricServiceClassPublicIngress:
|
||||
sections = []string{"organization", "sessions", "resources", "audit"}
|
||||
actions = []string{"read_organization_summary", "read_sessions"}
|
||||
case FabricServiceClassUserPortal:
|
||||
sections = []string{"profile", "sessions", "resources"}
|
||||
actions = []string{"read_profile", "read_sessions"}
|
||||
if scope == "user" {
|
||||
sections = []string{"profile", "sessions", "resources"}
|
||||
actions = []string{"read_profile", "read_sessions"}
|
||||
} else {
|
||||
actions = []string{"read_organization_summary", "read_sessions"}
|
||||
}
|
||||
}
|
||||
return map[string]any{
|
||||
"schema_version": adminRuntimeManifestSchema,
|
||||
@@ -729,6 +783,10 @@ func (m *Module) getNodeUpdatePlan(w http.ResponseWriter, r *http.Request) {
|
||||
InstallType: r.URL.Query().Get("install_type"),
|
||||
Channel: r.URL.Query().Get("channel"),
|
||||
ArtifactOrigin: requestOrigin(r),
|
||||
ExecutorCapabilities: append(
|
||||
r.URL.Query()["executor_capability"],
|
||||
r.URL.Query()["executor_capabilities"]...,
|
||||
),
|
||||
})
|
||||
if writeServiceError(w, err) {
|
||||
return
|
||||
@@ -736,6 +794,35 @@ func (m *Module) getNodeUpdatePlan(w http.ResponseWriter, r *http.Request) {
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"node_update_plan": item})
|
||||
}
|
||||
|
||||
func (m *Module) getNodeUpdateArtifactContent(w http.ResponseWriter, r *http.Request) {
|
||||
item, err := m.service.GetNodeUpdateArtifactContent(r.Context(), GetNodeUpdateArtifactContentInput{
|
||||
ClusterID: chi.URLParam(r, "clusterID"),
|
||||
NodeID: chi.URLParam(r, "nodeID"),
|
||||
ArtifactID: chi.URLParam(r, "artifactID"),
|
||||
Offset: parseInt64Query(r, "offset"),
|
||||
Length: parseInt64Query(r, "length"),
|
||||
})
|
||||
if writeServiceError(w, err) {
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, item)
|
||||
}
|
||||
|
||||
func parseInt64Query(r *http.Request, key string) int64 {
|
||||
if r == nil {
|
||||
return 0
|
||||
}
|
||||
value := strings.TrimSpace(r.URL.Query().Get(key))
|
||||
if value == "" {
|
||||
return 0
|
||||
}
|
||||
parsed, err := strconv.ParseInt(value, 10, 64)
|
||||
if err != nil || parsed < 0 {
|
||||
return 0
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
func requestOrigin(r *http.Request) string {
|
||||
proto := strings.TrimSpace(r.Header.Get("X-Forwarded-Proto"))
|
||||
if proto == "" {
|
||||
@@ -1732,35 +1819,35 @@ func (m *Module) getFabricServiceChannelPoolPolicy(w http.ResponseWriter, r *htt
|
||||
|
||||
func (m *Module) updateFabricServiceChannelPoolPolicy(w http.ResponseWriter, r *http.Request) {
|
||||
var payload struct {
|
||||
ActorUserID string `json:"actor_user_id"`
|
||||
EntryPoolNodeIDs []string `json:"entry_pool_node_ids"`
|
||||
ExitPoolNodeIDs []string `json:"exit_pool_node_ids"`
|
||||
PreferredEntryNodeID string `json:"preferred_entry_node_id"`
|
||||
PreferredExitNodeID string `json:"preferred_exit_node_id"`
|
||||
SelectionStrategy string `json:"selection_strategy"`
|
||||
RouteRebuild string `json:"route_rebuild"`
|
||||
EntryFailover string `json:"entry_failover"`
|
||||
ExitFailover string `json:"exit_failover"`
|
||||
BackendFallbackAllowed *bool `json:"backend_fallback_allowed"`
|
||||
StickySession *bool `json:"sticky_session"`
|
||||
ActorUserID string `json:"actor_user_id"`
|
||||
EntryPoolNodeIDs []string `json:"entry_pool_node_ids"`
|
||||
ExitPoolNodeIDs []string `json:"exit_pool_node_ids"`
|
||||
PreferredEntryNodeID string `json:"preferred_entry_node_id"`
|
||||
PreferredExitNodeID string `json:"preferred_exit_node_id"`
|
||||
SelectionStrategy string `json:"selection_strategy"`
|
||||
RouteRebuild string `json:"route_rebuild"`
|
||||
EntryFailover string `json:"entry_failover"`
|
||||
ExitFailover string `json:"exit_failover"`
|
||||
CompatFallbackAllowed *bool `json:"degraded_route_allowed"`
|
||||
StickySession *bool `json:"sticky_session"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
httpx.WriteError(w, http.StatusBadRequest, "invalid pool policy payload")
|
||||
return
|
||||
}
|
||||
item, err := m.service.UpdateFabricServiceChannelPoolPolicy(r.Context(), UpdateFabricServiceChannelPoolPolicyInput{
|
||||
ActorUserID: payload.ActorUserID,
|
||||
ClusterID: chi.URLParam(r, "clusterID"),
|
||||
EntryPoolNodeIDs: payload.EntryPoolNodeIDs,
|
||||
ExitPoolNodeIDs: payload.ExitPoolNodeIDs,
|
||||
PreferredEntryNodeID: payload.PreferredEntryNodeID,
|
||||
PreferredExitNodeID: payload.PreferredExitNodeID,
|
||||
SelectionStrategy: payload.SelectionStrategy,
|
||||
RouteRebuild: payload.RouteRebuild,
|
||||
EntryFailover: payload.EntryFailover,
|
||||
ExitFailover: payload.ExitFailover,
|
||||
BackendFallbackAllowed: payload.BackendFallbackAllowed,
|
||||
StickySession: payload.StickySession,
|
||||
ActorUserID: payload.ActorUserID,
|
||||
ClusterID: chi.URLParam(r, "clusterID"),
|
||||
EntryPoolNodeIDs: payload.EntryPoolNodeIDs,
|
||||
ExitPoolNodeIDs: payload.ExitPoolNodeIDs,
|
||||
PreferredEntryNodeID: payload.PreferredEntryNodeID,
|
||||
PreferredExitNodeID: payload.PreferredExitNodeID,
|
||||
SelectionStrategy: payload.SelectionStrategy,
|
||||
RouteRebuild: payload.RouteRebuild,
|
||||
EntryFailover: payload.EntryFailover,
|
||||
ExitFailover: payload.ExitFailover,
|
||||
CompatFallbackAllowed: payload.CompatFallbackAllowed,
|
||||
StickySession: payload.StickySession,
|
||||
})
|
||||
if writeServiceError(w, err) {
|
||||
return
|
||||
@@ -3411,7 +3498,7 @@ func writeServiceError(w http.ResponseWriter, err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
var legacyRemovalBlocked *LegacyRemovalBlockedError
|
||||
var standardCleanupBlocked *FabricStandardCleanupBlockedError
|
||||
switch {
|
||||
case errors.Is(err, ErrAccessDenied):
|
||||
httpx.WriteError(w, http.StatusForbidden, err.Error())
|
||||
@@ -3419,11 +3506,11 @@ func writeServiceError(w http.ResponseWriter, err error) bool {
|
||||
httpx.WriteError(w, http.StatusForbidden, err.Error())
|
||||
case errors.Is(err, ErrClusterReadOnly):
|
||||
httpx.WriteError(w, http.StatusConflict, err.Error())
|
||||
case errors.As(err, &legacyRemovalBlocked):
|
||||
case errors.As(err, &standardCleanupBlocked):
|
||||
httpx.WriteErrorMessage(w, http.StatusConflict, httpx.ErrorResponse{
|
||||
Error: httpx.NewErrorMessage(http.StatusConflict, err.Error(), legacyRemovalBlockedErrorDetails(*legacyRemovalBlocked), ""),
|
||||
Error: httpx.NewErrorMessage(http.StatusConflict, err.Error(), FabricStandardCleanupBlockedErrorDetails(*standardCleanupBlocked), ""),
|
||||
})
|
||||
case errors.Is(err, ErrLegacyRemovalBlocked):
|
||||
case errors.Is(err, ErrFabricStandardCleanupBlocked):
|
||||
httpx.WriteError(w, http.StatusConflict, err.Error())
|
||||
case errors.Is(err, ErrVPNLeaseAlreadyActive):
|
||||
httpx.WriteError(w, http.StatusConflict, err.Error())
|
||||
@@ -3437,24 +3524,31 @@ func writeServiceError(w http.ResponseWriter, err error) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func legacyRemovalBlockedErrorDetails(err LegacyRemovalBlockedError) map[string]any {
|
||||
func FabricStandardCleanupBlockedErrorDetails(err FabricStandardCleanupBlockedError) map[string]any {
|
||||
details := map[string]any{
|
||||
"blocked_operation": err.BlockedOperation,
|
||||
"legacy_removal_allowed": err.Report.LegacyRemovalAllowed,
|
||||
"bridge_hold_required": err.Report.BridgeHoldRequired,
|
||||
"bridge_hold_reasons": err.Report.BridgeHoldReasons,
|
||||
"blocked_operations": err.Report.BlockedOperations,
|
||||
"heartbeat_stale_after_seconds": err.Report.HeartbeatStaleAfterSeconds,
|
||||
"stale_nodes": err.Report.Summary.StaleNodes,
|
||||
"blocked_nodes": err.Report.Summary.BlockedNodes,
|
||||
"artifact_gap_nodes": err.Report.Summary.ArtifactGapNodes,
|
||||
"unknown_profile_nodes": err.Report.Summary.UnknownProfileNodes,
|
||||
"waiting_update_status_nodes": err.Report.Summary.WaitingUpdateStatusNodes,
|
||||
"unknown_version_nodes": err.Report.Summary.UnknownVersionNodes,
|
||||
"legacy_recovery_contract_nodes": err.Report.Summary.LegacyRecoveryContractNodes,
|
||||
"recovery_bridge_required_nodes": err.Report.Summary.RecoveryBridgeRequiredNodes,
|
||||
"blocked_operation": err.BlockedOperation,
|
||||
"fabric_standard_cleanup_allowed": err.Report.FabricStandardCleanupAllowed,
|
||||
"bridge_hold_required": err.Report.BridgeHoldRequired,
|
||||
"bridge_hold_reasons": err.Report.BridgeHoldReasons,
|
||||
"blocked_operations": err.Report.BlockedOperations,
|
||||
"heartbeat_stale_after_seconds": err.Report.HeartbeatStaleAfterSeconds,
|
||||
"stale_nodes": err.Report.Summary.StaleNodes,
|
||||
"blocked_nodes": err.Report.Summary.BlockedNodes,
|
||||
"artifact_gap_nodes": err.Report.Summary.ArtifactGapNodes,
|
||||
"area_diversity_alert_nodes": err.Report.Summary.AreaDiversityAlertNodes,
|
||||
"independent_ingress_alert_nodes": err.Report.Summary.IndependentIngressAlertNodes,
|
||||
"updater_wake_unsupported_nodes": err.Report.Summary.UpdaterWakeUnsupportedNodes,
|
||||
"updater_runtime_missing_nodes": err.Report.Summary.UpdaterRuntimeMissingNodes,
|
||||
"staged_self_update_pending_nodes": err.Report.Summary.StagedSelfUpdatePendingNodes,
|
||||
"standard_control_dependency_nodes": err.Report.Summary.StandardControlDependencyNodes,
|
||||
"registry_candidate_only_nodes": err.Report.Summary.RegistryCandidateOnlyNodes,
|
||||
"unknown_profile_nodes": err.Report.Summary.UnknownProfileNodes,
|
||||
"waiting_update_status_nodes": err.Report.Summary.WaitingUpdateStatusNodes,
|
||||
"unknown_version_nodes": err.Report.Summary.UnknownVersionNodes,
|
||||
"standard_recovery_contract_nodes": err.Report.Summary.StandardRecoveryContractNodes,
|
||||
"recovery_bridge_required_nodes": err.Report.Summary.RecoveryBridgeRequiredNodes,
|
||||
"recovery_bridge_replay_ready_nodes": err.Report.Summary.RecoveryBridgeReplayReadyNodes,
|
||||
"waiting_recovery_heartbeat_nodes": err.Report.Summary.WaitingRecoveryHeartbeatNodes,
|
||||
"waiting_recovery_heartbeat_nodes": err.Report.Summary.WaitingRecoveryHeartbeatNodes,
|
||||
}
|
||||
blockedNodeIDs := make([]string, 0, len(err.Report.Nodes))
|
||||
for _, node := range err.Report.Nodes {
|
||||
|
||||
@@ -18,9 +18,9 @@ func TestProjectAdminRuntimeReturnsReadOnlyManifest(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, "/clusters/cluster-1/nodes/node-1/admin-runtime/projection", bytes.NewReader([]byte(`{
|
||||
"schema_version":"rap.web_ingress.control_api_projection_request.v1",
|
||||
"method":"GET",
|
||||
"path":"/platform-admin/ui-manifest",
|
||||
"path":"/admin/ui-manifest",
|
||||
"scope":"platform",
|
||||
"service_class":"platform_admin"
|
||||
"service_class":"admin-ingress"
|
||||
}`)))
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
@@ -72,9 +72,9 @@ func TestProjectAdminRuntimeRejectsMutations(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, "/clusters/cluster-1/nodes/node-1/admin-runtime/projection", bytes.NewReader([]byte(`{
|
||||
"schema_version":"rap.web_ingress.control_api_projection_request.v1",
|
||||
"method":"POST",
|
||||
"path":"/platform-admin/nodes",
|
||||
"path":"/admin/nodes",
|
||||
"scope":"platform",
|
||||
"service_class":"platform_admin"
|
||||
"service_class":"admin-ingress"
|
||||
}`)))
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
@@ -106,7 +106,7 @@ func TestProjectAdminRuntimeReturnsHealthProjection(t *testing.T) {
|
||||
"method":"GET",
|
||||
"path":"/readyz",
|
||||
"scope":"platform",
|
||||
"service_class":"platform_admin"
|
||||
"service_class":"admin-ingress"
|
||||
}`)))
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
@@ -147,9 +147,9 @@ func TestProjectAdminRuntimeBlocksUnknownReadProjection(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, "/clusters/cluster-1/nodes/node-1/admin-runtime/projection", bytes.NewReader([]byte(`{
|
||||
"schema_version":"rap.web_ingress.control_api_projection_request.v1",
|
||||
"method":"GET",
|
||||
"path":"/platform-admin/nodes",
|
||||
"path":"/admin/nodes",
|
||||
"scope":"platform",
|
||||
"service_class":"platform_admin"
|
||||
"service_class":"admin-ingress"
|
||||
}`)))
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
@@ -181,9 +181,9 @@ func TestProjectAdminRuntimeRejectsScopeClassMismatch(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, "/clusters/cluster-1/nodes/node-1/admin-runtime/projection", bytes.NewReader([]byte(`{
|
||||
"schema_version":"rap.web_ingress.control_api_projection_request.v1",
|
||||
"method":"GET",
|
||||
"path":"/platform-admin/ui-manifest",
|
||||
"path":"/admin/ui-manifest",
|
||||
"scope":"organization",
|
||||
"service_class":"platform_admin"
|
||||
"service_class":"admin-ingress"
|
||||
}`)))
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
@@ -217,7 +217,7 @@ func TestProjectAdminRuntimeRejectsInvalidSchema(t *testing.T) {
|
||||
"method":"GET",
|
||||
"path":"/readyz",
|
||||
"scope":"platform",
|
||||
"service_class":"platform_admin"
|
||||
"service_class":"admin-ingress"
|
||||
}`)))
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -7,17 +7,17 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWriteServiceErrorLegacyRemovalBlockedIncludesBreakdownDetails(t *testing.T) {
|
||||
func TestWriteServiceErrorDisallowedRemovalBlockedIncludesBreakdownDetails(t *testing.T) {
|
||||
recorder := httptest.NewRecorder()
|
||||
handled := writeServiceError(recorder, &LegacyRemovalBlockedError{
|
||||
handled := writeServiceError(recorder, &FabricStandardCleanupBlockedError{
|
||||
BlockedOperation: "create_breaking_release",
|
||||
Report: StaleNodeRiskReport{
|
||||
HeartbeatStaleAfterSeconds: 900,
|
||||
LegacyRemovalAllowed: false,
|
||||
BridgeHoldRequired: true,
|
||||
BridgeHoldNodeIDs: []string{"node-1"},
|
||||
BridgeHoldReasons: []string{"legacy_contract_overlap"},
|
||||
BlockedOperations: []string{"create_breaking_release", "target_breaking_update_policy", "remove_recovery_bridge_overlap"},
|
||||
HeartbeatStaleAfterSeconds: 900,
|
||||
FabricStandardCleanupAllowed: false,
|
||||
BridgeHoldRequired: true,
|
||||
BridgeHoldNodeIDs: []string{"node-1"},
|
||||
BridgeHoldReasons: []string{"standard_contract_overlap"},
|
||||
BlockedOperations: []string{"create_breaking_release", "target_breaking_update_policy", "remove_recovery_bridge_overlap"},
|
||||
Nodes: []StaleNodeRiskNode{
|
||||
{NodeID: "node-1", Blocked: true, RecoveryBridgeRequired: true},
|
||||
{NodeID: "node-2", Blocked: false},
|
||||
@@ -25,11 +25,13 @@ func TestWriteServiceErrorLegacyRemovalBlockedIncludesBreakdownDetails(t *testin
|
||||
Summary: StaleNodeRiskSummary{
|
||||
StaleNodes: 1,
|
||||
BlockedNodes: 1,
|
||||
UpdaterRuntimeMissingNodes: 1,
|
||||
StagedSelfUpdatePendingNodes: 1,
|
||||
ArtifactGapNodes: 0,
|
||||
UnknownProfileNodes: 0,
|
||||
WaitingUpdateStatusNodes: 0,
|
||||
UnknownVersionNodes: 0,
|
||||
LegacyRecoveryContractNodes: 0,
|
||||
StandardRecoveryContractNodes: 0,
|
||||
WaitingRecoveryHeartbeatNodes: 1,
|
||||
},
|
||||
},
|
||||
@@ -54,6 +56,12 @@ func TestWriteServiceErrorLegacyRemovalBlockedIncludesBreakdownDetails(t *testin
|
||||
if payload.Error.Details["waiting_recovery_heartbeat_nodes"] != float64(1) {
|
||||
t.Fatalf("waiting_recovery_heartbeat_nodes = %v", payload.Error.Details["waiting_recovery_heartbeat_nodes"])
|
||||
}
|
||||
if payload.Error.Details["staged_self_update_pending_nodes"] != float64(1) {
|
||||
t.Fatalf("staged_self_update_pending_nodes = %v", payload.Error.Details["staged_self_update_pending_nodes"])
|
||||
}
|
||||
if payload.Error.Details["updater_runtime_missing_nodes"] != float64(1) {
|
||||
t.Fatalf("updater_runtime_missing_nodes = %v", payload.Error.Details["updater_runtime_missing_nodes"])
|
||||
}
|
||||
if payload.Error.Details["bridge_hold_required"] != true {
|
||||
t.Fatalf("bridge_hold_required = %v", payload.Error.Details["bridge_hold_required"])
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -311,6 +311,69 @@ func (s *PostgresStore) ListClusterNodes(ctx context.Context, clusterID string)
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (s *PostgresStore) RegisterFabricNode(ctx context.Context, input RegisterFabricNodeInput) (ClusterNode, error) {
|
||||
tx, err := s.db.Begin(ctx)
|
||||
if err != nil {
|
||||
return ClusterNode{}, err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
now := time.Now().UTC()
|
||||
nodeID := uuid.NewString()
|
||||
row := tx.QueryRow(ctx, `
|
||||
INSERT INTO nodes (
|
||||
id, owner_organization_id, node_key, name, ownership_type, registration_status, health_status,
|
||||
version_state, partition_state, reported_version, metadata, created_at, updated_at
|
||||
) VALUES ($1::uuid, $2::uuid, $3, $4, $5, 'active', 'healthy', 'current', 'healthy', $6, $7::jsonb, $8, $8)
|
||||
ON CONFLICT (node_key) DO UPDATE SET
|
||||
owner_organization_id = COALESCE(EXCLUDED.owner_organization_id, nodes.owner_organization_id),
|
||||
name = EXCLUDED.name,
|
||||
ownership_type = EXCLUDED.ownership_type,
|
||||
registration_status = 'active',
|
||||
health_status = 'healthy',
|
||||
reported_version = COALESCE(EXCLUDED.reported_version, nodes.reported_version),
|
||||
metadata = nodes.metadata || EXCLUDED.metadata,
|
||||
updated_at = EXCLUDED.updated_at
|
||||
RETURNING id::text
|
||||
`, nodeID, input.OwnerOrganizationID, input.NodeKey, input.Name, input.OwnershipType, input.ReportedVersion, []byte(input.Metadata), now)
|
||||
if err := row.Scan(&nodeID); err != nil {
|
||||
return ClusterNode{}, err
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(ctx, `
|
||||
INSERT INTO cluster_memberships (cluster_id, node_id, membership_status, joined_at, last_seen_at, metadata)
|
||||
VALUES ($1::uuid, $2::uuid, 'active', $3, $3, $4::jsonb)
|
||||
ON CONFLICT (cluster_id, node_id) DO UPDATE SET
|
||||
membership_status = 'active',
|
||||
last_seen_at = EXCLUDED.last_seen_at,
|
||||
metadata = cluster_memberships.metadata || EXCLUDED.metadata
|
||||
`, input.ClusterID, nodeID, now, []byte(`{"source":"fabric_control_register"}`)); err != nil {
|
||||
return ClusterNode{}, err
|
||||
}
|
||||
|
||||
itemRow := tx.QueryRow(ctx, `
|
||||
SELECT n.id::text, n.owner_organization_id::text, n.node_key, n.name, n.ownership_type,
|
||||
n.registration_status, n.health_status, n.version_state, n.partition_state,
|
||||
n.reported_version, n.last_seen_at, cm.membership_status, cm.metadata,
|
||||
ng.id::text, ng.name,
|
||||
n.created_at, n.updated_at
|
||||
FROM cluster_memberships cm
|
||||
JOIN nodes n ON n.id = cm.node_id
|
||||
LEFT JOIN cluster_node_group_memberships ngm ON ngm.cluster_id = cm.cluster_id AND ngm.node_id = cm.node_id
|
||||
LEFT JOIN cluster_node_groups ng ON ng.cluster_id = ngm.cluster_id AND ng.id = ngm.group_id
|
||||
WHERE cm.cluster_id = $1::uuid
|
||||
AND cm.node_id = $2::uuid
|
||||
`, input.ClusterID, nodeID)
|
||||
item, err := scanClusterNode(itemRow)
|
||||
if err != nil {
|
||||
return ClusterNode{}, err
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return ClusterNode{}, err
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (s *PostgresStore) ListNodeGroups(ctx context.Context, clusterID string) ([]ClusterNodeGroup, error) {
|
||||
rows, err := s.db.Query(ctx, `
|
||||
SELECT id::text, cluster_id::text, parent_group_id::text, name, description,
|
||||
@@ -511,7 +574,7 @@ func (s *PostgresStore) CreateJoinRequest(ctx context.Context, input CreateJoinR
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (s *PostgresStore) GetJoinRequestForBootstrap(ctx context.Context, input GetJoinRequestBootstrapInput) (NodeJoinRequest, error) {
|
||||
func (s *PostgresStore) GetJoinRequestForJoin(ctx context.Context, input GetJoinRequestJoinInput) (NodeJoinRequest, error) {
|
||||
row := s.db.QueryRow(ctx, `
|
||||
SELECT id::text, cluster_id::text, join_token_id::text, node_name, node_fingerprint, public_key,
|
||||
reported_capabilities, reported_facts, requested_roles, status, reviewed_by_user_id::text,
|
||||
@@ -666,7 +729,7 @@ func (s *PostgresStore) ApproveJoinRequest(ctx context.Context, input ApproveJoi
|
||||
}
|
||||
return ApprovedJoinRequest{
|
||||
JoinRequest: updated,
|
||||
Bootstrap: NodeBootstrap{
|
||||
JoinContract: NodeJoinContract{
|
||||
NodeID: nodeID,
|
||||
ClusterID: input.ClusterID,
|
||||
IdentityStatus: "active",
|
||||
@@ -1310,6 +1373,17 @@ func (s *PostgresStore) listReleaseArtifacts(ctx context.Context, releaseID stri
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (s *PostgresStore) GetReleaseArtifact(ctx context.Context, clusterID, artifactID string) (ReleaseArtifact, error) {
|
||||
row := s.db.QueryRow(ctx, `
|
||||
SELECT id::text, release_id::text, cluster_id::text, product, version, os, arch,
|
||||
install_type, kind, url, sha256, size_bytes, signature, metadata, created_at
|
||||
FROM release_artifacts
|
||||
WHERE cluster_id = $1::uuid
|
||||
AND id = $2::uuid
|
||||
`, clusterID, artifactID)
|
||||
return scanReleaseArtifact(row)
|
||||
}
|
||||
|
||||
func (s *PostgresStore) ListNodeUpdateServiceCandidates(ctx context.Context, clusterID string) ([]NodeUpdateServiceCandidate, error) {
|
||||
rows, err := s.db.Query(ctx, `
|
||||
SELECT n.id::text,
|
||||
@@ -4922,9 +4996,6 @@ func vpnEntryEndpointCandidatesFromHeartbeat(nodeID string, capabilities json.Ra
|
||||
item["tls_cert_sha256"] = certSHA256
|
||||
item["peer_cert_sha256"] = certSHA256
|
||||
}
|
||||
if apiBaseURL := vpnEntryAPIBaseURL(address); apiBaseURL != "" {
|
||||
item["api_base_url"] = apiBaseURL
|
||||
}
|
||||
out = append(out, item)
|
||||
}
|
||||
if len(out) == 0 {
|
||||
@@ -4943,9 +5014,6 @@ func vpnEntryEndpointCandidatesFromHeartbeat(nodeID string, capabilities json.Ra
|
||||
"status": "reported",
|
||||
"source": "node_latest_heartbeat.mesh_endpoint_report.peer_endpoint",
|
||||
}
|
||||
if apiBaseURL := vpnEntryAPIBaseURL(address); apiBaseURL != "" {
|
||||
item["api_base_url"] = apiBaseURL
|
||||
}
|
||||
out = append(out, item)
|
||||
}
|
||||
}
|
||||
@@ -5072,17 +5140,6 @@ func heartbeatCapabilityEnabled(capabilities json.RawMessage, name string) bool
|
||||
}
|
||||
}
|
||||
|
||||
func vpnEntryAPIBaseURL(address string) string {
|
||||
address = strings.TrimRight(strings.TrimSpace(address), "/")
|
||||
if address == "" {
|
||||
return ""
|
||||
}
|
||||
if !strings.HasPrefix(address, "http://") && !strings.HasPrefix(address, "https://") {
|
||||
return ""
|
||||
}
|
||||
return address + "/api/v1"
|
||||
}
|
||||
|
||||
func enrichVPNClientEntryEndpointCandidates(connection VPNClientConnection, endpoints map[string][]map[string]any) json.RawMessage {
|
||||
var cfg map[string]any
|
||||
if err := json.Unmarshal(connection.ClientConfig, &cfg); err != nil || cfg == nil {
|
||||
@@ -5346,7 +5403,7 @@ func enrichVPNClientFabricRoute(item VPNClientConnection, preferredEntryNodeID,
|
||||
entryPool := dedupeStrings(append([]string{}, item.EntryNodeIDs...))
|
||||
placementPolicy := jsonObjectFromRaw(item.PlacementPolicy)
|
||||
entrySelector, _ := placementPolicy["entry_selector"].(string)
|
||||
clientNodeEntry := strings.EqualFold(strings.TrimSpace(entrySelector), "client_node") || placementPolicy["android_node_agent_target"] == true
|
||||
clientNodeEntry := strings.EqualFold(strings.TrimSpace(entrySelector), "client_node") || placementPolicy["ipv4_ingress_node_target"] == true || placementPolicy["android_node_agent_target"] == true
|
||||
if len(entryPool) == 0 && !clientNodeEntry {
|
||||
entryPool = dedupeStrings(append([]string{}, item.AllowedNodeIDs...))
|
||||
}
|
||||
@@ -5450,7 +5507,7 @@ func enrichVPNClientFabricRoute(item VPNClientConnection, preferredEntryNodeID,
|
||||
"queue_policy": "bounded_queue_then_route_failover",
|
||||
"drop_policy": "drop_only_when_all_routes_unavailable_or_queue_full",
|
||||
"bulk_and_realtime": "same_packet_path",
|
||||
"flow_isolation": "hash_by_ip_protocol_and_ports",
|
||||
"flow_isolation": "opaque_packet_hash_shards",
|
||||
"target_dataplane": "fabric_farm_entry_to_exit_service_channel",
|
||||
"temporary_fallback": "none",
|
||||
},
|
||||
@@ -5519,8 +5576,8 @@ func vpnFabricRouteCandidates(entryPool, exitPool []string, selectedEntry, selec
|
||||
"role": role,
|
||||
"priority": priority,
|
||||
"status": "candidate",
|
||||
"source_role": "vpn-client",
|
||||
"route_scope": "client_node_to_exit_pool",
|
||||
"source_role": "ipv4-ingress",
|
||||
"route_scope": "ipv4_ingress_to_egress_pool",
|
||||
}
|
||||
if pair.entry != "" {
|
||||
candidate["entry_node_id"] = pair.entry
|
||||
|
||||
@@ -18,10 +18,10 @@ func TestMeshLatestObservationKeySeparatesRouteHealthByRoute(t *testing.T) {
|
||||
func TestMeshLatestObservationKeySeparatesConnectionManagerMode(t *testing.T) {
|
||||
key := meshLatestObservationKey(json.RawMessage(`{
|
||||
"observation_type":"peer_connection_manager",
|
||||
"transport_mode":"relay_control",
|
||||
"transport_mode":"relay_quic",
|
||||
"relay_node_id":"node-r"
|
||||
}`))
|
||||
if key != "peer_connection_manager:relay_control:node-r" {
|
||||
if key != "peer_connection_manager:relay_quic:node-r" {
|
||||
t.Fatalf("key = %q", key)
|
||||
}
|
||||
}
|
||||
@@ -192,7 +192,7 @@ func TestEnrichVPNClientEntryEndpointCandidatesAddsReportedQUICEndpoint(t *testi
|
||||
}
|
||||
}
|
||||
|
||||
func TestVPNEntryEndpointCandidatesKeepsQUICEndpointsAndRejectsLegacyHTTP(t *testing.T) {
|
||||
func TestVPNEntryEndpointCandidatesKeepsQUICEndpointsAndRejectsDisallowedHTTP(t *testing.T) {
|
||||
heartbeatMetadata := json.RawMessage(`{
|
||||
"mesh_endpoint_report": {
|
||||
"transport": "direct_quic",
|
||||
|
||||
@@ -29,7 +29,8 @@ type Repository interface {
|
||||
ExpireJoinTokens(ctx context.Context, clusterID string) error
|
||||
|
||||
CreateJoinRequest(ctx context.Context, input CreateJoinRequestInput, joinTokenID string) (NodeJoinRequest, error)
|
||||
GetJoinRequestForBootstrap(ctx context.Context, input GetJoinRequestBootstrapInput) (NodeJoinRequest, error)
|
||||
RegisterFabricNode(ctx context.Context, input RegisterFabricNodeInput) (ClusterNode, error)
|
||||
GetJoinRequestForJoin(ctx context.Context, input GetJoinRequestJoinInput) (NodeJoinRequest, error)
|
||||
ListJoinRequests(ctx context.Context, clusterID string) ([]NodeJoinRequest, error)
|
||||
ApproveJoinRequest(ctx context.Context, input ApproveJoinRequestInput) (ApprovedJoinRequest, error)
|
||||
SetJoinRequestApprovalAuthority(ctx context.Context, clusterID, joinRequestID string, payload json.RawMessage, signature ClusterSignature) (NodeJoinRequest, error)
|
||||
@@ -43,6 +44,7 @@ type Repository interface {
|
||||
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)
|
||||
GetReleaseArtifact(ctx context.Context, clusterID, artifactID string) (ReleaseArtifact, 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)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -2,13 +2,9 @@ package nodeagent
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
clustermodule "github.com/example/remote-access-platform/backend/internal/modules/cluster"
|
||||
"github.com/example/remote-access-platform/backend/internal/platform/httpx"
|
||||
@@ -17,7 +13,6 @@ import (
|
||||
)
|
||||
|
||||
type Module struct {
|
||||
db *pgxpool.Pool
|
||||
cluster *clustermodule.Service
|
||||
}
|
||||
|
||||
@@ -29,7 +24,6 @@ func NewModule(deps module.Dependencies) *Module {
|
||||
}
|
||||
}
|
||||
return &Module{
|
||||
db: deps.Infra.DB,
|
||||
cluster: clustermodule.NewService(clusterStore),
|
||||
}
|
||||
}
|
||||
@@ -40,62 +34,89 @@ 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("/docker-join-bundle", m.dockerJoinBundle)
|
||||
r.Post("/windows-join-bundle", m.windowsJoinBundle)
|
||||
r.Post("/linux-join-bundle", m.linuxJoinBundle)
|
||||
r.Post("/register", m.registerFabricNode)
|
||||
r.Post("/enroll", m.enrollAgent)
|
||||
r.Post("/enrollments/{requestID}/bootstrap", m.bootstrapEnrollment)
|
||||
r.Post("/register", m.registerAgent)
|
||||
r.Post("/{nodeID}/health", m.reportHealth)
|
||||
r.Post("/{nodeID}/services/status", m.reportServiceStatus)
|
||||
r.Post("/{nodeID}/update-manifest/request", m.requestUpdateManifest)
|
||||
r.Post("/{nodeID}/update-result", m.acknowledgeUpdateResult)
|
||||
r.Post("/{nodeID}/rollback-result", m.reportRollbackResult)
|
||||
r.Get("/{nodeID}/clusters/{clusterID}/vpn-assignments/desired", m.listVPNAssignments)
|
||||
r.Post("/{nodeID}/clusters/{clusterID}/vpn-assignments/{vpnConnectionID}/status", m.reportVPNAssignmentStatus)
|
||||
r.Post("/enrollments/{requestID}/join", m.fetchEnrollmentJoinContract)
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Module) linuxInstallProfile(w http.ResponseWriter, r *http.Request) {
|
||||
func (m *Module) linuxJoinBundle(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")
|
||||
httpx.WriteError(w, http.StatusBadRequest, "invalid linux join bundle payload")
|
||||
return
|
||||
}
|
||||
profile, err := m.cluster.GetLinuxInstallProfile(r.Context(), payload)
|
||||
bundle, err := m.cluster.GetLinuxJoinBundle(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})
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"join_bundle": bundle})
|
||||
}
|
||||
|
||||
func (m *Module) windowsInstallProfile(w http.ResponseWriter, r *http.Request) {
|
||||
func (m *Module) windowsJoinBundle(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")
|
||||
httpx.WriteError(w, http.StatusBadRequest, "invalid windows join bundle payload")
|
||||
return
|
||||
}
|
||||
profile, err := m.cluster.GetWindowsInstallProfile(r.Context(), payload)
|
||||
bundle, err := m.cluster.GetWindowsJoinBundle(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})
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"join_bundle": bundle})
|
||||
}
|
||||
|
||||
func (m *Module) dockerInstallProfile(w http.ResponseWriter, r *http.Request) {
|
||||
func (m *Module) dockerJoinBundle(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")
|
||||
httpx.WriteError(w, http.StatusBadRequest, "invalid docker join bundle payload")
|
||||
return
|
||||
}
|
||||
profile, err := m.cluster.GetDockerInstallProfile(r.Context(), payload)
|
||||
bundle, err := m.cluster.GetDockerJoinBundle(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})
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"join_bundle": bundle})
|
||||
}
|
||||
|
||||
func (m *Module) registerFabricNode(w http.ResponseWriter, r *http.Request) {
|
||||
var payload struct {
|
||||
ClusterID string `json:"cluster_id"`
|
||||
NodeKey string `json:"node_key"`
|
||||
Name string `json:"name"`
|
||||
OwnershipType string `json:"ownership_type"`
|
||||
OwnerOrganizationID *string `json:"owner_organization_id"`
|
||||
ReportedVersion *string `json:"reported_version"`
|
||||
Metadata json.RawMessage `json:"metadata"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
httpx.WriteError(w, http.StatusBadRequest, "invalid fabric node registration payload")
|
||||
return
|
||||
}
|
||||
item, err := m.cluster.RegisterFabricNode(r.Context(), clustermodule.RegisterFabricNodeInput{
|
||||
ClusterID: payload.ClusterID,
|
||||
NodeKey: payload.NodeKey,
|
||||
Name: payload.Name,
|
||||
OwnershipType: payload.OwnershipType,
|
||||
OwnerOrganizationID: payload.OwnerOrganizationID,
|
||||
ReportedVersion: payload.ReportedVersion,
|
||||
Metadata: payload.Metadata,
|
||||
})
|
||||
if err != nil {
|
||||
httpx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{
|
||||
"status": "registered",
|
||||
"node_id": item.ID,
|
||||
"node": item,
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Module) enrollAgent(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -133,17 +154,17 @@ func (m *Module) enrollAgent(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Module) bootstrapEnrollment(w http.ResponseWriter, r *http.Request) {
|
||||
func (m *Module) fetchEnrollmentJoinContract(w http.ResponseWriter, r *http.Request) {
|
||||
var payload struct {
|
||||
ClusterID string `json:"cluster_id"`
|
||||
NodeFingerprint string `json:"node_fingerprint"`
|
||||
PublicKey string `json:"public_key"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
httpx.WriteError(w, http.StatusBadRequest, "invalid enrollment bootstrap payload")
|
||||
httpx.WriteError(w, http.StatusBadRequest, "invalid enrollment join payload")
|
||||
return
|
||||
}
|
||||
result, err := m.cluster.GetJoinRequestBootstrap(r.Context(), clustermodule.GetJoinRequestBootstrapInput{
|
||||
result, err := m.cluster.GetJoinRequestJoin(r.Context(), clustermodule.GetJoinRequestJoinInput{
|
||||
ClusterID: payload.ClusterID,
|
||||
JoinRequestID: chi.URLParam(r, "requestID"),
|
||||
NodeFingerprint: payload.NodeFingerprint,
|
||||
@@ -155,261 +176,3 @@ func (m *Module) bootstrapEnrollment(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, result)
|
||||
}
|
||||
|
||||
func (m *Module) registerAgent(w http.ResponseWriter, r *http.Request) {
|
||||
var payload struct {
|
||||
ClusterID string `json:"cluster_id"`
|
||||
NodeKey string `json:"node_key"`
|
||||
Name string `json:"name"`
|
||||
OwnershipType string `json:"ownership_type"`
|
||||
OwnerOrganizationID *string `json:"owner_organization_id"`
|
||||
ReportedVersion *string `json:"reported_version"`
|
||||
Metadata json.RawMessage `json:"metadata"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
httpx.WriteError(w, http.StatusBadRequest, "invalid agent registration payload")
|
||||
return
|
||||
}
|
||||
if payload.NodeKey == "" || payload.Name == "" || payload.OwnershipType == "" {
|
||||
httpx.WriteError(w, http.StatusBadRequest, "node_key, name, and ownership_type are required")
|
||||
return
|
||||
}
|
||||
if len(payload.Metadata) == 0 {
|
||||
payload.Metadata = json.RawMessage(`{}`)
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
nodeID := uuid.NewString()
|
||||
if err := m.db.QueryRow(r.Context(), `
|
||||
INSERT INTO nodes (
|
||||
id, owner_organization_id, node_key, name, ownership_type, registration_status, health_status,
|
||||
version_state, partition_state, desired_version, reported_version, last_seen_at, metadata, created_at, updated_at
|
||||
) VALUES ($1, $2, $3, $4, $5, 'active', 'unknown', 'unknown', 'healthy', NULL, $6, $7, $8::jsonb, $9, $10)
|
||||
ON CONFLICT (node_key) DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
ownership_type = EXCLUDED.ownership_type,
|
||||
owner_organization_id = EXCLUDED.owner_organization_id,
|
||||
registration_status = 'active',
|
||||
reported_version = EXCLUDED.reported_version,
|
||||
last_seen_at = EXCLUDED.last_seen_at,
|
||||
metadata = EXCLUDED.metadata,
|
||||
updated_at = EXCLUDED.updated_at
|
||||
RETURNING id
|
||||
`, nodeID, payload.OwnerOrganizationID, payload.NodeKey, payload.Name, payload.OwnershipType, payload.ReportedVersion, now, []byte(payload.Metadata), now, now).Scan(&nodeID); err != nil {
|
||||
httpx.WriteError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if payload.ClusterID != "" {
|
||||
if _, err := m.db.Exec(r.Context(), `
|
||||
INSERT INTO cluster_memberships (cluster_id, node_id, membership_status, joined_at, last_seen_at, metadata)
|
||||
VALUES ($1::uuid, $2::uuid, 'active', $3, $3, $4::jsonb)
|
||||
ON CONFLICT (cluster_id, node_id) DO UPDATE SET
|
||||
membership_status = 'active',
|
||||
last_seen_at = EXCLUDED.last_seen_at,
|
||||
metadata = cluster_memberships.metadata || EXCLUDED.metadata
|
||||
`, payload.ClusterID, nodeID, now, []byte(`{"source":"fabric_control_candidate_registration"}`)); err != nil {
|
||||
httpx.WriteError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{
|
||||
"node_id": nodeID,
|
||||
"status": "registered",
|
||||
"legacy": true,
|
||||
"warning": "direct node-agent registration is retained for compatibility; production enrollment must use /node-agents/enroll",
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Module) reportHealth(w http.ResponseWriter, r *http.Request) {
|
||||
var payload struct {
|
||||
HealthStatus string `json:"health_status"`
|
||||
ReportedVersion *string `json:"reported_version"`
|
||||
Metadata json.RawMessage `json:"metadata"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
httpx.WriteError(w, http.StatusBadRequest, "invalid node health payload")
|
||||
return
|
||||
}
|
||||
if payload.HealthStatus == "" {
|
||||
payload.HealthStatus = "unknown"
|
||||
}
|
||||
if len(payload.Metadata) == 0 {
|
||||
payload.Metadata = json.RawMessage(`{}`)
|
||||
}
|
||||
if _, err := m.db.Exec(r.Context(), `
|
||||
UPDATE nodes
|
||||
SET health_status = $2, reported_version = COALESCE($3, reported_version), last_seen_at = $4, metadata = $5::jsonb, updated_at = $4
|
||||
WHERE id = $1
|
||||
`, chi.URLParam(r, "nodeID"), payload.HealthStatus, payload.ReportedVersion, time.Now().UTC(), []byte(payload.Metadata)); err != nil {
|
||||
httpx.WriteError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusAccepted, map[string]any{"status": "accepted"})
|
||||
}
|
||||
|
||||
func (m *Module) reportServiceStatus(w http.ResponseWriter, r *http.Request) {
|
||||
var payload struct {
|
||||
Services []struct {
|
||||
ServiceType string `json:"service_type"`
|
||||
ReportedState string `json:"reported_state"`
|
||||
Metadata json.RawMessage `json:"metadata"`
|
||||
} `json:"services"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
httpx.WriteError(w, http.StatusBadRequest, "invalid node service status payload")
|
||||
return
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
for _, service := range payload.Services {
|
||||
if len(service.Metadata) == 0 {
|
||||
service.Metadata = json.RawMessage(`{}`)
|
||||
}
|
||||
if _, err := m.db.Exec(r.Context(), `
|
||||
INSERT INTO node_services (
|
||||
node_id, service_type, enabled, desired_state, reported_state, last_reported_at, metadata, updated_at
|
||||
) VALUES ($1, $2, FALSE, 'disabled', $3, $4, $5::jsonb, $4)
|
||||
ON CONFLICT (node_id, service_type) DO UPDATE SET
|
||||
reported_state = EXCLUDED.reported_state,
|
||||
last_reported_at = EXCLUDED.last_reported_at,
|
||||
metadata = EXCLUDED.metadata,
|
||||
updated_at = EXCLUDED.updated_at
|
||||
`, chi.URLParam(r, "nodeID"), service.ServiceType, service.ReportedState, now, []byte(service.Metadata)); err != nil {
|
||||
httpx.WriteError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusAccepted, map[string]any{"status": "accepted"})
|
||||
}
|
||||
|
||||
func (m *Module) listVPNAssignments(w http.ResponseWriter, r *http.Request) {
|
||||
items, err := m.cluster.ListNodeVPNAssignments(r.Context(), chi.URLParam(r, "clusterID"), chi.URLParam(r, "nodeID"))
|
||||
if writeClusterServiceError(w, err) {
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{
|
||||
"vpn_assignments": items,
|
||||
"runtime_execution_enabled": false,
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Module) reportVPNAssignmentStatus(w http.ResponseWriter, r *http.Request) {
|
||||
var payload struct {
|
||||
ObservedStatus string `json:"observed_status"`
|
||||
StatusPayload json.RawMessage `json:"status_payload"`
|
||||
ObservedAt *time.Time `json:"observed_at"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
httpx.WriteError(w, http.StatusBadRequest, "invalid vpn assignment status payload")
|
||||
return
|
||||
}
|
||||
observedAt := time.Time{}
|
||||
if payload.ObservedAt != nil {
|
||||
observedAt = *payload.ObservedAt
|
||||
}
|
||||
item, err := m.cluster.ReportNodeVPNAssignmentStatus(r.Context(), clustermodule.ReportNodeVPNAssignmentStatusInput{
|
||||
ClusterID: chi.URLParam(r, "clusterID"),
|
||||
NodeID: chi.URLParam(r, "nodeID"),
|
||||
VPNConnectionID: chi.URLParam(r, "vpnConnectionID"),
|
||||
ObservedStatus: payload.ObservedStatus,
|
||||
StatusPayload: payload.StatusPayload,
|
||||
ObservedAt: observedAt,
|
||||
})
|
||||
if writeClusterServiceError(w, err) {
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusAccepted, map[string]any{
|
||||
"vpn_assignment_status": item,
|
||||
"runtime_execution_enabled": false,
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Module) requestUpdateManifest(w http.ResponseWriter, r *http.Request) {
|
||||
nodeID := chi.URLParam(r, "nodeID")
|
||||
var mode, channel string
|
||||
var canary, automatic bool
|
||||
var desiredVersion *string
|
||||
if err := m.db.QueryRow(r.Context(), `
|
||||
SELECT n.desired_version, p.mode, p.channel, p.canary, p.automatic_rollout
|
||||
FROM nodes n
|
||||
LEFT JOIN node_update_policies p ON p.node_id = n.id
|
||||
WHERE n.id = $1
|
||||
`, nodeID).Scan(&desiredVersion, &mode, &channel, &canary, &automatic); err != nil {
|
||||
httpx.WriteError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{
|
||||
"manifest": map[string]any{
|
||||
"node_id": nodeID,
|
||||
"desired_version": desiredVersion,
|
||||
"mode": mode,
|
||||
"channel": channel,
|
||||
"canary": canary,
|
||||
"automatic_rollout": automatic,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Module) acknowledgeUpdateResult(w http.ResponseWriter, r *http.Request) {
|
||||
m.recordUpdateRun(w, r, "update")
|
||||
}
|
||||
|
||||
func (m *Module) reportRollbackResult(w http.ResponseWriter, r *http.Request) {
|
||||
m.recordUpdateRun(w, r, "rollback")
|
||||
}
|
||||
|
||||
func (m *Module) recordUpdateRun(w http.ResponseWriter, r *http.Request, action string) {
|
||||
var payload struct {
|
||||
TargetVersion string `json:"target_version"`
|
||||
Status string `json:"status"`
|
||||
Payload json.RawMessage `json:"payload"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
httpx.WriteError(w, http.StatusBadRequest, "invalid update result payload")
|
||||
return
|
||||
}
|
||||
if payload.Status == "" {
|
||||
payload.Status = "acknowledged"
|
||||
}
|
||||
if len(payload.Payload) == 0 {
|
||||
payload.Payload = json.RawMessage(`{}`)
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
runID := uuid.NewString()
|
||||
if _, err := m.db.Exec(r.Context(), `
|
||||
INSERT INTO node_agent_update_runs (
|
||||
id, node_id, action, target_version, status, requested_at, acknowledged_at, completed_at, payload
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $6, CASE WHEN $5 IN ('succeeded', 'failed') THEN $6 ELSE NULL END, $7::jsonb)
|
||||
`, runID, chi.URLParam(r, "nodeID"), action, payload.TargetVersion, payload.Status, now, []byte(payload.Payload)); err != nil {
|
||||
httpx.WriteError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if action == "update" && payload.Status == "succeeded" {
|
||||
_, _ = m.db.Exec(r.Context(), `
|
||||
UPDATE nodes
|
||||
SET reported_version = $2, version_state = 'current', updated_at = $3
|
||||
WHERE id = $1
|
||||
`, chi.URLParam(r, "nodeID"), payload.TargetVersion, now)
|
||||
}
|
||||
if action == "rollback" && payload.Status == "succeeded" {
|
||||
_, _ = m.db.Exec(r.Context(), `
|
||||
UPDATE nodes
|
||||
SET reported_version = $2, version_state = 'rollback', updated_at = $3
|
||||
WHERE id = $1
|
||||
`, chi.URLParam(r, "nodeID"), payload.TargetVersion, now)
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusAccepted, map[string]any{"status": "accepted", "run_id": runID})
|
||||
}
|
||||
|
||||
func writeClusterServiceError(w http.ResponseWriter, err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
switch {
|
||||
case errors.Is(err, clustermodule.ErrVPNLeaseOwnerNotAllowed), errors.Is(err, clustermodule.ErrVPNLeaseOwnerRoleRequired):
|
||||
httpx.WriteError(w, http.StatusForbidden, err.Error())
|
||||
case errors.Is(err, clustermodule.ErrInvalidPayload):
|
||||
httpx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
default:
|
||||
httpx.WriteError(w, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
|
||||
const (
|
||||
ModeStrict = "strict"
|
||||
ModeLegacy = "legacy"
|
||||
ModeCompat = "compat"
|
||||
|
||||
ActivationSchemaVersion = "rap.installation.activation.v1"
|
||||
|
||||
@@ -60,7 +60,7 @@ type Verifier struct {
|
||||
func NewVerifier(cfg config.InstallationConfig) (*Verifier, error) {
|
||||
mode := strings.ToLower(strings.TrimSpace(cfg.AuthorityMode))
|
||||
if mode == "" {
|
||||
mode = ModeLegacy
|
||||
mode = ModeCompat
|
||||
}
|
||||
verifier := &Verifier{
|
||||
mode: mode,
|
||||
@@ -69,7 +69,7 @@ func NewVerifier(cfg config.InstallationConfig) (*Verifier, error) {
|
||||
}
|
||||
|
||||
switch mode {
|
||||
case ModeLegacy:
|
||||
case ModeCompat:
|
||||
return verifier, nil
|
||||
case ModeStrict:
|
||||
publicKey, err := decodeEd25519PublicKey(cfg.ProductRootPublicKeyBase64)
|
||||
@@ -87,7 +87,7 @@ func NewVerifier(cfg config.InstallationConfig) (*Verifier, error) {
|
||||
|
||||
func (v *Verifier) Mode() string {
|
||||
if v == nil || v.mode == "" {
|
||||
return ModeLegacy
|
||||
return ModeCompat
|
||||
}
|
||||
return v.mode
|
||||
}
|
||||
@@ -162,7 +162,7 @@ func EffectivePlatformRole(ctx context.Context, db postgresplatform.DBTX, verifi
|
||||
return PlatformRoleUser, nil
|
||||
}
|
||||
if verifier == nil || !verifier.Strict() {
|
||||
return legacyPlatformRole(ctx, db, userID)
|
||||
return storedPlatformRole(ctx, db, userID)
|
||||
}
|
||||
|
||||
var email string
|
||||
@@ -220,7 +220,7 @@ END, prg.granted_at DESC
|
||||
} else if ok {
|
||||
return role, nil
|
||||
}
|
||||
return legacyPlatformRole(ctx, db, userID)
|
||||
return storedPlatformRole(ctx, db, userID)
|
||||
}
|
||||
return bestRole, nil
|
||||
}
|
||||
@@ -257,7 +257,7 @@ WHERE u.id = $1::uuid
|
||||
}
|
||||
}
|
||||
|
||||
func legacyPlatformRole(ctx context.Context, db postgresplatform.DBTX, userID string) (string, error) {
|
||||
func storedPlatformRole(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 {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
|
||||
@@ -10,17 +10,18 @@ import (
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
App AppConfig
|
||||
HTTP HTTPConfig
|
||||
Postgres PostgresConfig
|
||||
Redis RedisConfig
|
||||
Auth AuthConfig
|
||||
Installation InstallationConfig
|
||||
DataPlane DataPlaneConfig
|
||||
Secret SecretConfig
|
||||
Session SessionConfig
|
||||
Worker WorkerConfig
|
||||
WebSocket WebSocketConfig
|
||||
App AppConfig
|
||||
HTTP HTTPConfig
|
||||
FabricControl FabricControlConfig
|
||||
Postgres PostgresConfig
|
||||
Redis RedisConfig
|
||||
Auth AuthConfig
|
||||
Installation InstallationConfig
|
||||
DataPlane DataPlaneConfig
|
||||
Secret SecretConfig
|
||||
Session SessionConfig
|
||||
Worker WorkerConfig
|
||||
WebSocket WebSocketConfig
|
||||
}
|
||||
|
||||
type AppConfig struct {
|
||||
@@ -37,6 +38,11 @@ type HTTPConfig struct {
|
||||
ShutdownTimeout time.Duration
|
||||
}
|
||||
|
||||
type FabricControlConfig struct {
|
||||
Enabled bool
|
||||
ListenAddr string
|
||||
}
|
||||
|
||||
type PostgresConfig struct {
|
||||
DSN string
|
||||
MaxConns int32
|
||||
@@ -118,6 +124,10 @@ func Load() (Config, error) {
|
||||
IdleTimeout: getDuration("HTTP_IDLE_TIMEOUT", 60*time.Second),
|
||||
ShutdownTimeout: getDuration("HTTP_SHUTDOWN_TIMEOUT", 10*time.Second),
|
||||
},
|
||||
FabricControl: FabricControlConfig{
|
||||
Enabled: getBool("FABRIC_CONTROL_QUIC_ENABLED", false),
|
||||
ListenAddr: getEnv("FABRIC_CONTROL_QUIC_LISTEN_ADDR", ":19191"),
|
||||
},
|
||||
Postgres: PostgresConfig{
|
||||
DSN: getEnv("POSTGRES_DSN", ""),
|
||||
MaxConns: int32(getInt("POSTGRES_MAX_CONNS", 20)),
|
||||
@@ -235,13 +245,13 @@ func Load() (Config, error) {
|
||||
func normalizeInstallationAuthorityMode(mode string, rootPublicKey string) string {
|
||||
mode = strings.ToLower(strings.TrimSpace(mode))
|
||||
switch mode {
|
||||
case "strict", "legacy":
|
||||
case "strict", "compat":
|
||||
return mode
|
||||
case "":
|
||||
if strings.TrimSpace(rootPublicKey) != "" {
|
||||
return "strict"
|
||||
}
|
||||
return "legacy"
|
||||
return "compat"
|
||||
default:
|
||||
return mode
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package fabriccontrol
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
const (
|
||||
frameMagic uint32 = 0x52415046
|
||||
frameVersion uint8 = 1
|
||||
frameHeaderSize = 32
|
||||
maxPayload = 1024 * 1024
|
||||
|
||||
frameData uint8 = 5
|
||||
trafficClassReliable uint8 = 4
|
||||
controlForwardQUICStream uint64 = 3
|
||||
)
|
||||
|
||||
type frame struct {
|
||||
Type uint8
|
||||
TrafficClass uint8
|
||||
StreamID uint64
|
||||
Sequence uint64
|
||||
Payload []byte
|
||||
}
|
||||
|
||||
var errInvalidFrame = errors.New("invalid fabric frame")
|
||||
|
||||
func readFrame(r io.Reader) (frame, error) {
|
||||
header := make([]byte, frameHeaderSize)
|
||||
if _, err := io.ReadFull(r, header); err != nil {
|
||||
return frame{}, err
|
||||
}
|
||||
if binary.BigEndian.Uint32(header[0:4]) != frameMagic || header[4] != frameVersion {
|
||||
return frame{}, errInvalidFrame
|
||||
}
|
||||
payloadLen := int(binary.BigEndian.Uint32(header[28:32]))
|
||||
if payloadLen > maxPayload {
|
||||
return frame{}, fmt.Errorf("%w: payload too large", errInvalidFrame)
|
||||
}
|
||||
out := frame{
|
||||
Type: header[5],
|
||||
TrafficClass: header[8],
|
||||
StreamID: binary.BigEndian.Uint64(header[12:20]),
|
||||
Sequence: binary.BigEndian.Uint64(header[20:28]),
|
||||
}
|
||||
if payloadLen > 0 {
|
||||
out.Payload = make([]byte, payloadLen)
|
||||
if _, err := io.ReadFull(r, out.Payload); err != nil {
|
||||
return frame{}, err
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func writeFrame(w io.Writer, f frame) error {
|
||||
if len(f.Payload) > maxPayload {
|
||||
return fmt.Errorf("%w: payload too large", errInvalidFrame)
|
||||
}
|
||||
header := make([]byte, frameHeaderSize)
|
||||
binary.BigEndian.PutUint32(header[0:4], frameMagic)
|
||||
header[4] = frameVersion
|
||||
header[5] = f.Type
|
||||
header[8] = f.TrafficClass
|
||||
binary.BigEndian.PutUint64(header[12:20], f.StreamID)
|
||||
binary.BigEndian.PutUint64(header[20:28], f.Sequence)
|
||||
binary.BigEndian.PutUint32(header[28:32], uint32(len(f.Payload)))
|
||||
if _, err := w.Write(header); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(f.Payload) == 0 {
|
||||
return nil
|
||||
}
|
||||
_, err := w.Write(f.Payload)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
package fabriccontrol
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/quic-go/quic-go"
|
||||
)
|
||||
|
||||
const nextProto = "rap-fabric-data-session-v1"
|
||||
|
||||
type Config struct {
|
||||
Enabled bool
|
||||
ListenAddr string
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
cfg Config
|
||||
router http.Handler
|
||||
ln *quic.Listener
|
||||
}
|
||||
|
||||
type rawControlRequest struct {
|
||||
Method string `json:"method"`
|
||||
Path string `json:"path"`
|
||||
Body json.RawMessage `json:"body,omitempty"`
|
||||
}
|
||||
|
||||
type rawControlResponse struct {
|
||||
StatusCode int `json:"status_code"`
|
||||
Body json.RawMessage `json:"body,omitempty"`
|
||||
}
|
||||
|
||||
type controlEnvelope struct {
|
||||
Payload json.RawMessage `json:"payload,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func New(cfg Config, router http.Handler) *Server {
|
||||
return &Server{cfg: cfg, router: router}
|
||||
}
|
||||
|
||||
func (s *Server) ListenAndServe(ctx context.Context) error {
|
||||
if s == nil || !s.cfg.Enabled {
|
||||
return nil
|
||||
}
|
||||
listenAddr := strings.TrimSpace(s.cfg.ListenAddr)
|
||||
if listenAddr == "" {
|
||||
listenAddr = ":19191"
|
||||
}
|
||||
ln, err := quic.ListenAddr(listenAddr, selfSignedTLSConfig(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.ln = ln
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
_ = ln.Close()
|
||||
}()
|
||||
for {
|
||||
conn, err := ln.Accept(ctx)
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
go s.handleConn(ctx, conn)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Close() error {
|
||||
if s == nil || s.ln == nil {
|
||||
return nil
|
||||
}
|
||||
return s.ln.Close()
|
||||
}
|
||||
|
||||
func (s *Server) handleConn(ctx context.Context, conn *quic.Conn) {
|
||||
for {
|
||||
stream, err := conn.AcceptStream(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go s.handleStream(ctx, stream)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleStream(ctx context.Context, stream *quic.Stream) {
|
||||
defer stream.Close()
|
||||
for {
|
||||
reqFrame, err := readFrame(stream)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if reqFrame.Type != frameData || reqFrame.StreamID != controlForwardQUICStream {
|
||||
continue
|
||||
}
|
||||
payload, err := s.handlePayload(ctx, reqFrame.Payload)
|
||||
envelope := controlEnvelope{Payload: payload}
|
||||
if err != nil {
|
||||
envelope = controlEnvelope{Error: err.Error()}
|
||||
}
|
||||
raw, _ := json.Marshal(envelope)
|
||||
_ = writeFrame(stream, frame{
|
||||
Type: frameData,
|
||||
TrafficClass: trafficClassReliable,
|
||||
StreamID: controlForwardQUICStream,
|
||||
Sequence: reqFrame.Sequence,
|
||||
Payload: raw,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handlePayload(ctx context.Context, payload []byte) (json.RawMessage, error) {
|
||||
var req rawControlRequest
|
||||
if err := json.Unmarshal(payload, &req); err != nil {
|
||||
return nil, fmt.Errorf("invalid fabric control request")
|
||||
}
|
||||
method := strings.ToUpper(strings.TrimSpace(req.Method))
|
||||
if method == "" {
|
||||
method = http.MethodGet
|
||||
}
|
||||
path := normalizeControlPath(req.Path)
|
||||
if path == "" {
|
||||
return nil, fmt.Errorf("fabric control path is not allowed")
|
||||
}
|
||||
httpReq := httptest.NewRequest(method, path, bytes.NewReader(req.Body)).WithContext(ctx)
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("X-RAP-Fabric-Control", "quic")
|
||||
rec := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(rec, httpReq)
|
||||
body := append(json.RawMessage(nil), rec.Body.Bytes()...)
|
||||
raw, err := json.Marshal(rawControlResponse{StatusCode: rec.Code, Body: body})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
func normalizeControlPath(path string) string {
|
||||
path = strings.TrimSpace(path)
|
||||
if path == "" || strings.Contains(path, "://") || strings.Contains(path, "..") {
|
||||
return ""
|
||||
}
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
if strings.HasPrefix(path, "/api/v1/") {
|
||||
return path
|
||||
}
|
||||
switch {
|
||||
case strings.HasPrefix(path, "/clusters/"),
|
||||
strings.HasPrefix(path, "/organizations/"),
|
||||
strings.HasPrefix(path, "/node-agents/"),
|
||||
strings.HasPrefix(path, "/auth/"):
|
||||
return "/api/v1" + path
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func selfSignedTLSConfig() *tls.Config {
|
||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
tmpl := x509.Certificate{
|
||||
SerialNumber: big.NewInt(time.Now().UnixNano()),
|
||||
Subject: pkix.Name{CommonName: "rap-fabric-control"},
|
||||
NotBefore: time.Now().Add(-time.Hour),
|
||||
NotAfter: time.Now().Add(365 * 24 * time.Hour),
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
}
|
||||
der, err := x509.CreateCertificate(rand.Reader, &tmpl, &tmpl, &key.PublicKey, key)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
cert := tls.Certificate{Certificate: [][]byte{der}, PrivateKey: key}
|
||||
return &tls.Config{Certificates: []tls.Certificate{cert}, NextProtos: []string{nextProto}}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"github.com/example/remote-access-platform/backend/internal/modules/worker"
|
||||
"github.com/example/remote-access-platform/backend/internal/platform/authority"
|
||||
"github.com/example/remote-access-platform/backend/internal/platform/config"
|
||||
"github.com/example/remote-access-platform/backend/internal/platform/fabriccontrol"
|
||||
"github.com/example/remote-access-platform/backend/internal/platform/httpserver"
|
||||
"github.com/example/remote-access-platform/backend/internal/platform/logging"
|
||||
"github.com/example/remote-access-platform/backend/internal/platform/module"
|
||||
@@ -33,12 +34,13 @@ import (
|
||||
)
|
||||
|
||||
type App struct {
|
||||
cfg config.Config
|
||||
logger *slog.Logger
|
||||
httpServer *http.Server
|
||||
workers []backgroundRunner
|
||||
db closeFunc
|
||||
redis closeFunc
|
||||
cfg config.Config
|
||||
logger *slog.Logger
|
||||
httpServer *http.Server
|
||||
fabricControl *fabriccontrol.Server
|
||||
workers []backgroundRunner
|
||||
db closeFunc
|
||||
redis closeFunc
|
||||
}
|
||||
|
||||
type closeFunc func() error
|
||||
@@ -138,7 +140,11 @@ func NewApp(ctx context.Context) (*App, error) {
|
||||
cfg: cfg,
|
||||
logger: logger,
|
||||
httpServer: httpserver.New(cfg.HTTP, router),
|
||||
workers: []backgroundRunner{workerEvents.Run, leaseMonitor.Run},
|
||||
fabricControl: fabriccontrol.New(fabriccontrol.Config{
|
||||
Enabled: cfg.FabricControl.Enabled,
|
||||
ListenAddr: cfg.FabricControl.ListenAddr,
|
||||
}, router),
|
||||
workers: []backgroundRunner{workerEvents.Run, leaseMonitor.Run},
|
||||
db: func() error {
|
||||
db.Close()
|
||||
return nil
|
||||
@@ -167,6 +173,14 @@ func (a *App) Run(ctx context.Context) error {
|
||||
}
|
||||
}()
|
||||
}
|
||||
if a.fabricControl != nil && a.cfg.FabricControl.Enabled {
|
||||
go func() {
|
||||
a.logger.Info("fabric control quic starting", "addr", a.cfg.FabricControl.ListenAddr, "service", a.cfg.App.Name)
|
||||
if err := a.fabricControl.ListenAndServe(ctx); err != nil {
|
||||
errCh <- err
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@@ -184,6 +198,9 @@ func (a *App) Run(ctx context.Context) error {
|
||||
if err := a.httpServer.Shutdown(shutdownCtx); err != nil {
|
||||
return fmt.Errorf("shutdown http server: %w", err)
|
||||
}
|
||||
if a.fabricControl != nil {
|
||||
_ = a.fabricControl.Close()
|
||||
}
|
||||
|
||||
if err := a.redis(); err != nil {
|
||||
return fmt.Errorf("close redis: %w", err)
|
||||
|
||||
Reference in New Issue
Block a user