рабочий вариант, но скороть 10 МБит
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user