mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-08 01:37:54 +00:00
712 lines
22 KiB
Go
712 lines
22 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/mock"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/truenas"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
|
|
)
|
|
|
|
const trueNASConnectionsPathPrefix = "/api/truenas/connections/"
|
|
|
|
// TrueNASHandlers manages TrueNAS connection CRUD and connectivity checks.
|
|
type TrueNASHandlers struct {
|
|
getPersistence func(ctx context.Context) *config.ConfigPersistence
|
|
getConfig func(ctx context.Context) *config.Config
|
|
getMonitor func(ctx context.Context) *monitoring.Monitor
|
|
getPoller func(ctx context.Context) *monitoring.TrueNASPoller
|
|
newClient func(truenas.ClientConfig) (trueNASClient, error)
|
|
}
|
|
|
|
type trueNASConnectionResponse struct {
|
|
config.TrueNASInstance
|
|
Poll *monitoring.TrueNASConnectionPollStatus `json:"poll,omitempty"`
|
|
Observed *monitoring.TrueNASConnectionObservedSummary `json:"observed,omitempty"`
|
|
}
|
|
|
|
type trueNASClient interface {
|
|
TestConnection(ctx context.Context) error
|
|
Close()
|
|
}
|
|
|
|
// HandleAdd stores a new TrueNAS connection.
|
|
func (h *TrueNASHandlers) HandleAdd(w http.ResponseWriter, r *http.Request) {
|
|
if !h.featureEnabled(w) {
|
|
return
|
|
}
|
|
if mock.IsMockEnabled() {
|
|
writeErrorResponse(w, http.StatusForbidden, "mock_mode_enabled", "Cannot modify connections in mock mode", nil)
|
|
return
|
|
}
|
|
|
|
r.Body = http.MaxBytesReader(w, r.Body, 32*1024)
|
|
defer r.Body.Close()
|
|
|
|
instance, ok := decodeTrueNASInstanceRequest(w, r, config.NewTrueNASInstance())
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
instance.ID = strings.TrimSpace(instance.ID)
|
|
if instance.ID == "" {
|
|
instance.ID = config.NewTrueNASInstance().ID
|
|
}
|
|
normalizeTrueNASInstance(&instance)
|
|
|
|
if err := instance.Validate(); err != nil {
|
|
writeErrorResponse(w, http.StatusBadRequest, "validation_error", err.Error(), nil)
|
|
return
|
|
}
|
|
|
|
persistence := h.persistenceForRequest(w, r.Context())
|
|
if persistence == nil {
|
|
return
|
|
}
|
|
|
|
if h.enforceMonitoredSystemLimit(w, r, instance) {
|
|
return
|
|
}
|
|
|
|
existing, err := persistence.LoadTrueNASConfig()
|
|
if err != nil {
|
|
writeErrorResponse(w, http.StatusInternalServerError, "truenas_load_failed", "Failed to load TrueNAS configuration", map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
existing = append(existing, instance)
|
|
if err := persistence.SaveTrueNASConfig(existing); err != nil {
|
|
writeErrorResponse(w, http.StatusInternalServerError, "truenas_save_failed", "Failed to save TrueNAS configuration", map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
redacted := instance.Redacted()
|
|
writeJSON(w, http.StatusCreated, redacted)
|
|
}
|
|
|
|
// HandleList returns all configured TrueNAS connections with sensitive fields redacted.
|
|
func (h *TrueNASHandlers) HandleList(w http.ResponseWriter, r *http.Request) {
|
|
if !h.featureEnabled(w) {
|
|
return
|
|
}
|
|
if mock.IsMockEnabled() {
|
|
writeJSON(w, http.StatusOK, mockTrueNASConnectionResponses())
|
|
return
|
|
}
|
|
|
|
persistence := h.persistenceForRequest(w, r.Context())
|
|
if persistence == nil {
|
|
return
|
|
}
|
|
|
|
instances, err := persistence.LoadTrueNASConfig()
|
|
if err != nil {
|
|
writeErrorResponse(w, http.StatusInternalServerError, "truenas_load_failed", "Failed to load TrueNAS configuration", map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
orgID := resolveTenantOrgID(r)
|
|
var summaries map[string]monitoring.TrueNASConnectionSummary
|
|
if h != nil && h.getPoller != nil {
|
|
if poller := h.getPoller(r.Context()); poller != nil {
|
|
summaries = poller.ConnectionSummaries(orgID, instances)
|
|
}
|
|
}
|
|
|
|
redacted := make([]trueNASConnectionResponse, 0, len(instances))
|
|
for i := range instances {
|
|
item := instances[i]
|
|
item.ApplyDefaults()
|
|
response := trueNASConnectionResponse{
|
|
TrueNASInstance: item.Redacted(),
|
|
}
|
|
if summary, ok := summaries[strings.TrimSpace(item.ID)]; ok {
|
|
response.Poll = summary.Poll
|
|
response.Observed = summary.Observed
|
|
}
|
|
redacted = append(redacted, response)
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, redacted)
|
|
}
|
|
|
|
// HandleDelete removes a configured TrueNAS connection by ID.
|
|
func (h *TrueNASHandlers) HandleDelete(w http.ResponseWriter, r *http.Request) {
|
|
if !h.featureEnabled(w) {
|
|
return
|
|
}
|
|
if mock.IsMockEnabled() {
|
|
writeErrorResponse(w, http.StatusForbidden, "mock_mode_enabled", "Cannot modify connections in mock mode", nil)
|
|
return
|
|
}
|
|
|
|
connectionID, ok := trueNASConnectionIDFromPath(r.URL.Path)
|
|
if !ok {
|
|
writeErrorResponse(w, http.StatusBadRequest, "missing_connection_id", "Connection ID is required", nil)
|
|
return
|
|
}
|
|
|
|
persistence := h.persistenceForRequest(w, r.Context())
|
|
if persistence == nil {
|
|
return
|
|
}
|
|
|
|
instances, err := persistence.LoadTrueNASConfig()
|
|
if err != nil {
|
|
writeErrorResponse(w, http.StatusInternalServerError, "truenas_load_failed", "Failed to load TrueNAS configuration", map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
index := -1
|
|
for i := range instances {
|
|
if strings.TrimSpace(instances[i].ID) == connectionID {
|
|
index = i
|
|
break
|
|
}
|
|
}
|
|
if index < 0 {
|
|
writeErrorResponse(w, http.StatusNotFound, "truenas_not_found", "Connection not found", nil)
|
|
return
|
|
}
|
|
|
|
instances = append(instances[:index], instances[index+1:]...)
|
|
if err := persistence.SaveTrueNASConfig(instances); err != nil {
|
|
writeErrorResponse(w, http.StatusInternalServerError, "truenas_save_failed", "Failed to save TrueNAS configuration", map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"success": true,
|
|
"id": connectionID,
|
|
})
|
|
}
|
|
|
|
// HandleUpdate replaces a configured TrueNAS connection by ID while preserving
|
|
// unchanged masked secrets from the stored record.
|
|
func (h *TrueNASHandlers) HandleUpdate(w http.ResponseWriter, r *http.Request) {
|
|
if !h.featureEnabled(w) {
|
|
return
|
|
}
|
|
if mock.IsMockEnabled() {
|
|
writeErrorResponse(w, http.StatusForbidden, "mock_mode_enabled", "Cannot modify connections in mock mode", nil)
|
|
return
|
|
}
|
|
|
|
connectionID, ok := trueNASConnectionIDFromPath(r.URL.Path)
|
|
if !ok {
|
|
writeErrorResponse(w, http.StatusBadRequest, "missing_connection_id", "Connection ID is required", nil)
|
|
return
|
|
}
|
|
|
|
persistence := h.persistenceForRequest(w, r.Context())
|
|
if persistence == nil {
|
|
return
|
|
}
|
|
|
|
instances, err := persistence.LoadTrueNASConfig()
|
|
if err != nil {
|
|
writeErrorResponse(w, http.StatusInternalServerError, "truenas_load_failed", "Failed to load TrueNAS configuration", map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
index := -1
|
|
for i := range instances {
|
|
if strings.TrimSpace(instances[i].ID) == connectionID {
|
|
index = i
|
|
break
|
|
}
|
|
}
|
|
if index < 0 {
|
|
writeErrorResponse(w, http.StatusNotFound, "truenas_not_found", "Connection not found", nil)
|
|
return
|
|
}
|
|
|
|
instance, ok := decodeTrueNASInstanceRequest(w, r, instances[index])
|
|
if !ok {
|
|
return
|
|
}
|
|
instance.ID = connectionID
|
|
normalizeTrueNASInstance(&instance)
|
|
instance.PreserveMaskedSecrets(instances[index])
|
|
if err := instance.Validate(); err != nil {
|
|
writeErrorResponse(w, http.StatusBadRequest, "validation_error", err.Error(), nil)
|
|
return
|
|
}
|
|
if h.enforceMonitoredSystemLimitReplacement(w, r, instances[index], instance) {
|
|
return
|
|
}
|
|
|
|
instances[index] = instance
|
|
if err := persistence.SaveTrueNASConfig(instances); err != nil {
|
|
writeErrorResponse(w, http.StatusInternalServerError, "truenas_save_failed", "Failed to save TrueNAS configuration", map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, instance.Redacted())
|
|
}
|
|
|
|
// HandlePreviewConnection projects the monitored-system impact of a proposed
|
|
// TrueNAS connection without persisting it.
|
|
func (h *TrueNASHandlers) HandlePreviewConnection(w http.ResponseWriter, r *http.Request) {
|
|
if !h.featureEnabled(w) {
|
|
return
|
|
}
|
|
|
|
instance, ok := decodeTrueNASInstanceRequest(w, r, config.NewTrueNASInstance())
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
normalizeTrueNASInstance(&instance)
|
|
if err := instance.Validate(); err != nil {
|
|
writeErrorResponse(w, http.StatusBadRequest, "validation_error", err.Error(), nil)
|
|
return
|
|
}
|
|
|
|
monitor := h.monitorForRequest(r.Context())
|
|
usage := monitoredSystemUsage(monitor)
|
|
if !usage.available {
|
|
writeMonitoredSystemUsageUnavailable(w, usage.unavailableReason)
|
|
return
|
|
}
|
|
|
|
candidate := trueNASMonitoredSystemCandidate(instance)
|
|
preview := unifiedresources.PreviewMonitoredSystemCandidate(usage.readState, candidate)
|
|
if candidate.CountsTowardMonitoredSystems() && len(preview.ProjectedSystems) == 0 {
|
|
writeErrorResponse(w, http.StatusBadRequest, "validation_error", "TrueNAS connection did not resolve to a canonical monitored system preview", nil)
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, monitoredSystemLedgerPreviewResponse(r.Context(), false, preview).NormalizeCollections())
|
|
}
|
|
|
|
// HandlePreviewSavedConnection projects the monitored-system impact of an edit
|
|
// to one saved TrueNAS connection using the same secret-preservation path as
|
|
// update and saved test flows.
|
|
func (h *TrueNASHandlers) HandlePreviewSavedConnection(w http.ResponseWriter, r *http.Request) {
|
|
if !h.featureEnabled(w) {
|
|
return
|
|
}
|
|
|
|
connectionID, ok := trueNASConnectionIDFromPreviewPath(r.URL.Path)
|
|
if !ok {
|
|
writeErrorResponse(w, http.StatusBadRequest, "missing_connection_id", "Connection ID is required", nil)
|
|
return
|
|
}
|
|
|
|
persistence := h.persistenceForRequest(w, r.Context())
|
|
if persistence == nil {
|
|
return
|
|
}
|
|
|
|
instances, err := persistence.LoadTrueNASConfig()
|
|
if err != nil {
|
|
writeErrorResponse(w, http.StatusInternalServerError, "truenas_load_failed", "Failed to load TrueNAS configuration", map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
for i := range instances {
|
|
current := instances[i]
|
|
if strings.TrimSpace(current.ID) != connectionID {
|
|
continue
|
|
}
|
|
|
|
payload, hasPayload, ok := decodeOptionalTrueNASInstanceRequest(w, r, current)
|
|
if !ok {
|
|
return
|
|
}
|
|
next := current
|
|
if hasPayload {
|
|
payload.ID = connectionID
|
|
normalizeTrueNASInstance(&payload)
|
|
payload.PreserveMaskedSecrets(current)
|
|
if err := payload.Validate(); err != nil {
|
|
writeErrorResponse(w, http.StatusBadRequest, "validation_error", err.Error(), nil)
|
|
return
|
|
}
|
|
next = payload
|
|
}
|
|
|
|
monitor := h.monitorForRequest(r.Context())
|
|
usage := monitoredSystemUsage(monitor)
|
|
if !usage.available {
|
|
writeMonitoredSystemUsageUnavailable(w, usage.unavailableReason)
|
|
return
|
|
}
|
|
|
|
candidate := trueNASMonitoredSystemCandidate(next)
|
|
replacementHost := pulseTokenHostCandidate(current.Host)
|
|
preview := unifiedresources.PreviewMonitoredSystemCandidateReplacement(
|
|
usage.readState,
|
|
unifiedresources.MonitoredSystemReplacement{
|
|
Source: unifiedresources.SourceTrueNAS,
|
|
Selector: unifiedresources.MonitoredSystemReplacementSelector{
|
|
Hostname: replacementHost,
|
|
},
|
|
},
|
|
candidate,
|
|
)
|
|
if candidate.CountsTowardMonitoredSystems() && len(preview.ProjectedSystems) == 0 {
|
|
writeErrorResponse(w, http.StatusBadRequest, "validation_error", "TrueNAS connection did not resolve to a canonical monitored system preview", nil)
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, monitoredSystemLedgerPreviewResponse(r.Context(), true, preview).NormalizeCollections())
|
|
return
|
|
}
|
|
|
|
writeErrorResponse(w, http.StatusNotFound, "truenas_not_found", "Connection not found", nil)
|
|
}
|
|
|
|
// HandleTestConnection validates connectivity for a proposed TrueNAS connection.
|
|
func (h *TrueNASHandlers) HandleTestConnection(w http.ResponseWriter, r *http.Request) {
|
|
if !h.featureEnabled(w) {
|
|
return
|
|
}
|
|
|
|
instance, ok := decodeTrueNASInstanceRequest(w, r, config.NewTrueNASInstance())
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
normalizeTrueNASInstance(&instance)
|
|
if err := instance.Validate(); err != nil {
|
|
writeErrorResponse(w, http.StatusBadRequest, "validation_error", err.Error(), nil)
|
|
return
|
|
}
|
|
|
|
h.writeConnectionTestResult(w, r, instance)
|
|
}
|
|
|
|
// HandleTestSavedConnection validates connectivity for one saved TrueNAS
|
|
// connection using the server-owned stored secret material instead of relying
|
|
// on frontend redaction placeholders.
|
|
func (h *TrueNASHandlers) HandleTestSavedConnection(w http.ResponseWriter, r *http.Request) {
|
|
if !h.featureEnabled(w) {
|
|
return
|
|
}
|
|
|
|
connectionID, ok := trueNASConnectionIDFromTestPath(r.URL.Path)
|
|
if !ok {
|
|
writeErrorResponse(w, http.StatusBadRequest, "missing_connection_id", "Connection ID is required", nil)
|
|
return
|
|
}
|
|
|
|
persistence := h.persistenceForRequest(w, r.Context())
|
|
if persistence == nil {
|
|
return
|
|
}
|
|
|
|
instances, err := persistence.LoadTrueNASConfig()
|
|
if err != nil {
|
|
writeErrorResponse(w, http.StatusInternalServerError, "truenas_load_failed", "Failed to load TrueNAS configuration", map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
for i := range instances {
|
|
instance := instances[i]
|
|
if strings.TrimSpace(instance.ID) != connectionID {
|
|
continue
|
|
}
|
|
normalizeTrueNASInstance(&instance)
|
|
payload, hasPayload, ok := decodeOptionalTrueNASInstanceRequest(w, r, instance)
|
|
if !ok {
|
|
return
|
|
} else if hasPayload {
|
|
payload.ID = connectionID
|
|
normalizeTrueNASInstance(&payload)
|
|
payload.PreserveMaskedSecrets(instance)
|
|
if err := payload.Validate(); err != nil {
|
|
writeErrorResponse(w, http.StatusBadRequest, "validation_error", err.Error(), nil)
|
|
return
|
|
}
|
|
instance = payload
|
|
}
|
|
invalidConfig, err := h.testConnectionInstance(r, instance)
|
|
if err != nil {
|
|
if !hasPayload && h != nil && h.getPoller != nil {
|
|
if poller := h.getPoller(r.Context()); poller != nil {
|
|
poller.RecordConnectionTestFailure(resolveTenantOrgID(r), connectionID, instance, err, time.Now().UTC())
|
|
}
|
|
}
|
|
if invalidConfig {
|
|
writeErrorResponse(w, http.StatusBadRequest, "truenas_invalid_config", "Invalid TrueNAS connection configuration", map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
writeErrorResponse(w, http.StatusBadRequest, "truenas_connection_failed", "Failed to connect to TrueNAS", map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
if !hasPayload && h != nil && h.getPoller != nil {
|
|
if poller := h.getPoller(r.Context()); poller != nil {
|
|
poller.RecordConnectionTestSuccess(resolveTenantOrgID(r), connectionID, instance, time.Now().UTC())
|
|
}
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"success": true})
|
|
return
|
|
}
|
|
|
|
writeErrorResponse(w, http.StatusNotFound, "truenas_not_found", "Connection not found", nil)
|
|
}
|
|
|
|
func (h *TrueNASHandlers) writeConnectionTestResult(
|
|
w http.ResponseWriter,
|
|
r *http.Request,
|
|
instance config.TrueNASInstance,
|
|
) {
|
|
invalidConfig, err := h.testConnectionInstance(r, instance)
|
|
if err != nil {
|
|
if invalidConfig {
|
|
writeErrorResponse(w, http.StatusBadRequest, "truenas_invalid_config", "Invalid TrueNAS connection configuration", map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
writeErrorResponse(w, http.StatusBadRequest, "truenas_connection_failed", "Failed to connect to TrueNAS", map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"success": true})
|
|
}
|
|
|
|
func (h *TrueNASHandlers) testConnectionInstance(
|
|
r *http.Request,
|
|
instance config.TrueNASInstance,
|
|
) (bool, error) {
|
|
newClient := h.newClient
|
|
if newClient == nil {
|
|
newClient = func(cfg truenas.ClientConfig) (trueNASClient, error) { return truenas.NewClient(cfg) }
|
|
}
|
|
|
|
client, err := newClient(truenas.ClientConfig{
|
|
Host: instance.Host,
|
|
Port: instance.Port,
|
|
APIKey: instance.APIKey,
|
|
Username: instance.Username,
|
|
Password: instance.Password,
|
|
UseHTTPS: instance.UseHTTPS,
|
|
InsecureSkipVerify: instance.InsecureSkipVerify,
|
|
Fingerprint: instance.Fingerprint,
|
|
Timeout: 10 * time.Second,
|
|
})
|
|
if err != nil {
|
|
return true, err
|
|
}
|
|
defer client.Close()
|
|
|
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
if err := client.TestConnection(ctx); err != nil {
|
|
return false, err
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
func normalizeTrueNASInstance(instance *config.TrueNASInstance) {
|
|
if instance == nil {
|
|
return
|
|
}
|
|
instance.Name = strings.TrimSpace(instance.Name)
|
|
instance.Host = strings.TrimSpace(instance.Host)
|
|
instance.APIKey = strings.TrimSpace(instance.APIKey)
|
|
instance.Username = strings.TrimSpace(instance.Username)
|
|
instance.Password = strings.TrimSpace(instance.Password)
|
|
instance.Fingerprint = strings.TrimSpace(instance.Fingerprint)
|
|
instance.ApplyDefaults()
|
|
}
|
|
|
|
func decodeTrueNASInstanceRequest(
|
|
w http.ResponseWriter,
|
|
r *http.Request,
|
|
base config.TrueNASInstance,
|
|
) (config.TrueNASInstance, bool) {
|
|
r.Body = http.MaxBytesReader(w, r.Body, 32*1024)
|
|
defer r.Body.Close()
|
|
|
|
instance := base
|
|
if err := json.NewDecoder(r.Body).Decode(&instance); err != nil {
|
|
writeErrorResponse(w, http.StatusBadRequest, "invalid_request", "Invalid request body", map[string]string{"error": err.Error()})
|
|
return config.TrueNASInstance{}, false
|
|
}
|
|
|
|
return instance, true
|
|
}
|
|
|
|
func decodeOptionalTrueNASInstanceRequest(
|
|
w http.ResponseWriter,
|
|
r *http.Request,
|
|
base config.TrueNASInstance,
|
|
) (config.TrueNASInstance, bool, bool) {
|
|
r.Body = http.MaxBytesReader(w, r.Body, 32*1024)
|
|
defer r.Body.Close()
|
|
|
|
body, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
writeErrorResponse(w, http.StatusBadRequest, "invalid_request", "Invalid request body", map[string]string{"error": err.Error()})
|
|
return config.TrueNASInstance{}, false, false
|
|
}
|
|
if len(bytes.TrimSpace(body)) == 0 {
|
|
return base, false, true
|
|
}
|
|
|
|
instance := base
|
|
if err := json.Unmarshal(body, &instance); err != nil {
|
|
writeErrorResponse(w, http.StatusBadRequest, "invalid_request", "Invalid request body", map[string]string{"error": err.Error()})
|
|
return config.TrueNASInstance{}, false, false
|
|
}
|
|
|
|
return instance, true, true
|
|
}
|
|
|
|
func (h *TrueNASHandlers) featureEnabled(w http.ResponseWriter) bool {
|
|
if truenas.IsFeatureEnabled() {
|
|
return true
|
|
}
|
|
writeErrorResponse(w, http.StatusNotFound, "truenas_disabled", "TrueNAS integration has been explicitly disabled", nil)
|
|
return false
|
|
}
|
|
|
|
func (h *TrueNASHandlers) monitorForRequest(ctx context.Context) *monitoring.Monitor {
|
|
if h == nil || h.getMonitor == nil {
|
|
return nil
|
|
}
|
|
return h.getMonitor(ctx)
|
|
}
|
|
|
|
func (h *TrueNASHandlers) persistenceForRequest(w http.ResponseWriter, ctx context.Context) *config.ConfigPersistence {
|
|
if h == nil || h.getPersistence == nil {
|
|
writeErrorResponse(w, http.StatusInternalServerError, "truenas_unavailable", "TrueNAS service unavailable", nil)
|
|
return nil
|
|
}
|
|
persistence := h.getPersistence(ctx)
|
|
if persistence == nil {
|
|
writeErrorResponse(w, http.StatusInternalServerError, "truenas_unavailable", "TrueNAS service unavailable", nil)
|
|
return nil
|
|
}
|
|
return persistence
|
|
}
|
|
|
|
func (h *TrueNASHandlers) enforceMonitoredSystemLimit(
|
|
w http.ResponseWriter,
|
|
r *http.Request,
|
|
instance config.TrueNASInstance,
|
|
) bool {
|
|
var monitor *monitoring.Monitor
|
|
if h != nil && h.getMonitor != nil {
|
|
monitor = h.getMonitor(r.Context())
|
|
}
|
|
|
|
decision := monitoredSystemLimitDecisionForCandidate(r.Context(), monitor, trueNASMonitoredSystemCandidate(instance))
|
|
if !decision.usageAvailable {
|
|
writeMonitoredSystemUsageUnavailable(w, decision.usageUnavailableReason)
|
|
return true
|
|
}
|
|
if !decision.exceeded {
|
|
return false
|
|
}
|
|
|
|
emitLimitBlockedEvent(r.Context(), decision.current, decision.limit)
|
|
writeMaxMonitoredSystemsLimitExceeded(w, decision)
|
|
return true
|
|
}
|
|
|
|
func (h *TrueNASHandlers) enforceMonitoredSystemLimitReplacement(
|
|
w http.ResponseWriter,
|
|
r *http.Request,
|
|
current config.TrueNASInstance,
|
|
next config.TrueNASInstance,
|
|
) bool {
|
|
var monitor *monitoring.Monitor
|
|
if h != nil && h.getMonitor != nil {
|
|
monitor = h.getMonitor(r.Context())
|
|
}
|
|
|
|
replacementHost := pulseTokenHostCandidate(current.Host)
|
|
decision := monitoredSystemLimitDecisionForCandidateReplacement(r.Context(), monitor, unifiedresources.MonitoredSystemReplacement{
|
|
Source: unifiedresources.SourceTrueNAS,
|
|
Selector: unifiedresources.MonitoredSystemReplacementSelector{
|
|
Hostname: replacementHost,
|
|
},
|
|
}, trueNASMonitoredSystemCandidate(next))
|
|
if !decision.usageAvailable {
|
|
writeMonitoredSystemUsageUnavailable(w, decision.usageUnavailableReason)
|
|
return true
|
|
}
|
|
if !decision.exceeded {
|
|
return false
|
|
}
|
|
|
|
emitLimitBlockedEvent(r.Context(), decision.current, decision.limit)
|
|
writeMaxMonitoredSystemsLimitExceeded(w, decision)
|
|
return true
|
|
}
|
|
|
|
func trueNASMonitoredSystemCandidate(instance config.TrueNASInstance) unifiedresources.MonitoredSystemCandidate {
|
|
return unifiedresources.MonitoredSystemCandidate{
|
|
Source: unifiedresources.SourceTrueNAS,
|
|
Type: unifiedresources.ResourceTypeAgent,
|
|
Name: instance.Name,
|
|
Hostname: pulseTokenHostCandidate(instance.Host),
|
|
HostURL: instance.Host,
|
|
State: monitoredSystemCandidateStateFromEnabled(instance.Enabled),
|
|
}
|
|
}
|
|
|
|
func trueNASConnectionIDFromPath(path string) (string, bool) {
|
|
if !strings.HasPrefix(path, trueNASConnectionsPathPrefix) {
|
|
return "", false
|
|
}
|
|
|
|
raw := strings.Trim(strings.TrimPrefix(path, trueNASConnectionsPathPrefix), "/")
|
|
if raw == "" || strings.Contains(raw, "/") {
|
|
return "", false
|
|
}
|
|
|
|
connectionID, err := url.PathUnescape(raw)
|
|
if err != nil {
|
|
return "", false
|
|
}
|
|
connectionID = strings.TrimSpace(connectionID)
|
|
if connectionID == "" || strings.Contains(connectionID, "/") {
|
|
return "", false
|
|
}
|
|
|
|
return connectionID, true
|
|
}
|
|
|
|
func trueNASConnectionIDFromTestPath(path string) (string, bool) {
|
|
return trueNASConnectionIDFromActionPath(path, "/test")
|
|
}
|
|
|
|
func trueNASConnectionIDFromPreviewPath(path string) (string, bool) {
|
|
return trueNASConnectionIDFromActionPath(path, "/preview")
|
|
}
|
|
|
|
func trueNASConnectionIDFromActionPath(path string, suffix string) (string, bool) {
|
|
if !strings.HasPrefix(path, trueNASConnectionsPathPrefix) {
|
|
return "", false
|
|
}
|
|
trimmed := strings.Trim(strings.TrimPrefix(path, trueNASConnectionsPathPrefix), "/")
|
|
if !strings.HasSuffix(trimmed, suffix) {
|
|
return "", false
|
|
}
|
|
raw := strings.TrimSuffix(trimmed, suffix)
|
|
if raw == "" || strings.Contains(raw, "/") {
|
|
return "", false
|
|
}
|
|
connectionID, err := url.PathUnescape(raw)
|
|
if err != nil {
|
|
return "", false
|
|
}
|
|
connectionID = strings.TrimSpace(connectionID)
|
|
if connectionID == "" || strings.Contains(connectionID, "/") {
|
|
return "", false
|
|
}
|
|
return connectionID, true
|
|
}
|