Files
pulse/internal/api/vmware_handlers.go
T
rcourtman 5016cbc2ba Add vSphere network inventory
Project vCenter network inventory through canonical resources and add the vSphere Networks table backed by vCenter network topology. Align resource presentation coalescing so state and resource APIs share the same host contract.
2026-05-22 20:26:56 +01:00

889 lines
26 KiB
Go

package api
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"net/url"
"strings"
"sync"
"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/unifiedresources"
"github.com/rcourtman/pulse-go-rewrite/internal/vmware"
)
const vmwareConnectionsPathPrefix = "/api/vmware/connections/"
// VMwareHandlers manages VMware vCenter connection CRUD and connectivity checks.
type VMwareHandlers struct {
getPersistence func(ctx context.Context) *config.ConfigPersistence
getMonitor func(ctx context.Context) *monitoring.Monitor
getPoller func(ctx context.Context) *monitoring.VMwarePoller
newClient func(vmware.ClientConfig) (vmwareClient, error)
previewRecords func(ctx context.Context, instance config.VMwareVCenterInstance) ([]unifiedresources.IngestRecord, error)
statusMu sync.RWMutex
statuses map[string]vmwareConnectionRuntimeStatus
}
type vmwareClient interface {
TestConnection(ctx context.Context) (*vmware.InventorySummary, error)
Close()
}
type vmwareConnectionResponse struct {
config.VMwareVCenterInstance
Poll *monitoring.VMwareConnectionPollStatus `json:"poll,omitempty"`
Observed *monitoring.VMwareConnectionObservedSummary `json:"observed,omitempty"`
}
type vmwareConnectionRuntimeStatus struct {
Poll *monitoring.VMwareConnectionPollStatus
Observed *monitoring.VMwareConnectionObservedSummary
}
// HandleAdd stores a new VMware vCenter connection.
func (h *VMwareHandlers) 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 := decodeVMwareInstanceRequest(w, r, config.NewVMwareVCenterInstance())
if !ok {
return
}
instance.ID = strings.TrimSpace(instance.ID)
if instance.ID == "" {
instance.ID = config.NewVMwareVCenterInstance().ID
}
normalizeVMwareInstance(&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
}
instances, err := persistence.LoadVMwareConfig()
if err != nil {
writeErrorResponse(w, http.StatusInternalServerError, "vmware_load_failed", "Failed to load VMware configuration", map[string]string{"error": err.Error()})
return
}
instances = append(instances, instance)
if err := persistence.SaveVMwareConfig(instances); err != nil {
writeErrorResponse(w, http.StatusInternalServerError, "vmware_save_failed", "Failed to save VMware configuration", map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusCreated, instance.Redacted())
}
// HandleList returns all configured VMware connections with sensitive fields redacted.
func (h *VMwareHandlers) HandleList(w http.ResponseWriter, r *http.Request) {
if !h.featureEnabled(w) {
return
}
if mock.IsMockEnabled() {
writeJSON(w, http.StatusOK, mockVMwareConnectionResponses())
return
}
persistence := h.persistenceForRequest(w, r.Context())
if persistence == nil {
return
}
instances, err := persistence.LoadVMwareConfig()
if err != nil {
writeErrorResponse(w, http.StatusInternalServerError, "vmware_load_failed", "Failed to load VMware configuration", map[string]string{"error": err.Error()})
return
}
orgID := resolveTenantOrgID(r)
var summaries map[string]monitoring.VMwareConnectionSummary
if h != nil && h.getPoller != nil {
if poller := h.getPoller(r.Context()); poller != nil {
summaries = poller.ConnectionSummaries(orgID, instances)
}
}
responses := make([]vmwareConnectionResponse, 0, len(instances))
for i := range instances {
item := instances[i]
item.ApplyDefaults()
response := vmwareConnectionResponse{
VMwareVCenterInstance: item.Redacted(),
}
connID := strings.TrimSpace(item.ID)
if summary, ok := summaries[connID]; ok {
response.Poll = summary.Poll
response.Observed = summary.Observed
} else {
status := h.runtimeStatus(connID)
response.Poll = status.Poll
response.Observed = status.Observed
}
responses = append(responses, response)
}
writeJSON(w, http.StatusOK, responses)
}
// HandleDelete removes a configured VMware vCenter connection by ID.
func (h *VMwareHandlers) 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 := vmwareConnectionIDFromPath(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.LoadVMwareConfig()
if err != nil {
writeErrorResponse(w, http.StatusInternalServerError, "vmware_load_failed", "Failed to load VMware 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, "vmware_not_found", "Connection not found", nil)
return
}
instances = append(instances[:index], instances[index+1:]...)
if err := persistence.SaveVMwareConfig(instances); err != nil {
writeErrorResponse(w, http.StatusInternalServerError, "vmware_save_failed", "Failed to save VMware configuration", map[string]string{"error": err.Error()})
return
}
h.clearRuntimeStatus(connectionID)
writeJSON(w, http.StatusOK, map[string]any{
"success": true,
"id": connectionID,
})
}
// HandleUpdate replaces a configured VMware vCenter connection by ID while
// preserving unchanged masked secrets from the stored record.
func (h *VMwareHandlers) 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 := vmwareConnectionIDFromPath(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.LoadVMwareConfig()
if err != nil {
writeErrorResponse(w, http.StatusInternalServerError, "vmware_load_failed", "Failed to load VMware 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, "vmware_not_found", "Connection not found", nil)
return
}
instance, ok := decodeVMwareInstanceRequest(w, r, instances[index])
if !ok {
return
}
instance.ID = connectionID
normalizeVMwareInstance(&instance)
instance.PreserveMaskedSecrets(instances[index])
if err := instance.Validate(); err != nil {
writeErrorResponse(w, http.StatusBadRequest, "validation_error", err.Error(), nil)
return
}
instances[index] = instance
if err := persistence.SaveVMwareConfig(instances); err != nil {
writeErrorResponse(w, http.StatusInternalServerError, "vmware_save_failed", "Failed to save VMware configuration", map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusOK, instance.Redacted())
}
// HandlePreviewConnection projects the monitored-system impact of a proposed
// VMware vCenter connection without persisting it.
func (h *VMwareHandlers) HandlePreviewConnection(w http.ResponseWriter, r *http.Request) {
if !h.featureEnabled(w) {
return
}
instance, ok := decodeVMwareInstanceRequest(w, r, config.NewVMwareVCenterInstance())
if !ok {
return
}
normalizeVMwareInstance(&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 := vmwareMonitoredSystemCandidate(instance)
if !candidate.CountsTowardMonitoredSystems() {
preview := unifiedresources.PreviewMonitoredSystemCandidate(usage.readState, candidate)
writeJSON(w, http.StatusOK, monitoredSystemLedgerPreviewResponse(false, preview).NormalizeCollections())
return
}
records, invalidConfig, err := h.previewMonitoredSystemRecords(r.Context(), instance)
if err != nil {
h.writeConnectionFailure(w, invalidConfig, err)
return
}
preview := unifiedresources.PreviewMonitoredSystemRecords(usage.readState, map[unifiedresources.DataSource][]unifiedresources.IngestRecord{
unifiedresources.SourceVMware: records,
})
if len(preview.ProjectedSystems) == 0 {
writeErrorResponse(w, http.StatusBadRequest, "validation_error", "VMware connection did not resolve to a canonical monitored system preview", nil)
return
}
writeJSON(w, http.StatusOK, monitoredSystemLedgerPreviewResponse(false, preview).NormalizeCollections())
}
// HandlePreviewSavedConnection projects the monitored-system impact of editing
// one saved VMware connection using the same secret-preservation path as
// update and saved test flows.
func (h *VMwareHandlers) HandlePreviewSavedConnection(w http.ResponseWriter, r *http.Request) {
if !h.featureEnabled(w) {
return
}
connectionID, ok := vmwareConnectionIDFromPreviewPath(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.LoadVMwareConfig()
if err != nil {
writeErrorResponse(w, http.StatusInternalServerError, "vmware_load_failed", "Failed to load VMware 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 := decodeOptionalVMwareInstanceRequest(w, r, current)
if !ok {
return
}
next := current
if hasPayload {
payload.ID = connectionID
normalizeVMwareInstance(&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 := vmwareMonitoredSystemCandidate(next)
replacement := unifiedresources.MonitoredSystemReplacement{
Source: unifiedresources.SourceVMware,
Selector: unifiedresources.MonitoredSystemReplacementSelector{
ResourceID: connectionID,
},
}
if !candidate.CountsTowardMonitoredSystems() {
preview := unifiedresources.PreviewMonitoredSystemCandidateReplacement(
usage.readState,
replacement,
candidate,
)
writeJSON(w, http.StatusOK, monitoredSystemLedgerPreviewResponse(true, preview).NormalizeCollections())
return
}
records, invalidConfig, err := h.previewMonitoredSystemRecords(r.Context(), next)
if err != nil {
h.writeConnectionFailure(w, invalidConfig, err)
return
}
preview := unifiedresources.PreviewMonitoredSystemRecordsReplacement(
usage.readState,
replacement,
map[unifiedresources.DataSource][]unifiedresources.IngestRecord{
unifiedresources.SourceVMware: records,
},
)
if len(preview.ProjectedSystems) == 0 {
writeErrorResponse(w, http.StatusBadRequest, "validation_error", "VMware connection did not resolve to a canonical monitored system preview", nil)
return
}
writeJSON(w, http.StatusOK, monitoredSystemLedgerPreviewResponse(true, preview).NormalizeCollections())
return
}
writeErrorResponse(w, http.StatusNotFound, "vmware_not_found", "Connection not found", nil)
}
// HandleTestConnection validates connectivity for a proposed VMware vCenter connection.
func (h *VMwareHandlers) HandleTestConnection(w http.ResponseWriter, r *http.Request) {
if !h.featureEnabled(w) {
return
}
instance, ok := decodeVMwareInstanceRequest(w, r, config.NewVMwareVCenterInstance())
if !ok {
return
}
normalizeVMwareInstance(&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 VMware
// connection using the server-owned stored secret material instead of relying
// on frontend redaction placeholders.
func (h *VMwareHandlers) HandleTestSavedConnection(w http.ResponseWriter, r *http.Request) {
if !h.featureEnabled(w) {
return
}
connectionID, ok := vmwareConnectionIDFromTestPath(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.LoadVMwareConfig()
if err != nil {
writeErrorResponse(w, http.StatusInternalServerError, "vmware_load_failed", "Failed to load VMware configuration", map[string]string{"error": err.Error()})
return
}
for i := range instances {
instance := instances[i]
if strings.TrimSpace(instance.ID) != connectionID {
continue
}
normalizeVMwareInstance(&instance)
payload, hasPayload, ok := decodeOptionalVMwareInstanceRequest(w, r, instance)
if !ok {
return
}
if hasPayload {
payload.ID = connectionID
normalizeVMwareInstance(&payload)
payload.PreserveMaskedSecrets(instance)
if err := payload.Validate(); err != nil {
writeErrorResponse(w, http.StatusBadRequest, "validation_error", err.Error(), nil)
return
}
instance = payload
}
summary, invalidConfig, err := h.testConnectionInstance(r, instance)
if err != nil {
if !hasPayload {
at := time.Now().UTC()
if poller := h.pollerForRequest(r.Context()); poller != nil {
poller.RecordConnectionTestFailure(resolveTenantOrgID(r), connectionID, err, at)
} else {
h.recordTestFailure(connectionID, err, at)
}
}
h.writeConnectionFailure(w, invalidConfig, err)
return
}
if !hasPayload {
at := time.Now().UTC()
if poller := h.pollerForRequest(r.Context()); poller != nil {
poller.RecordConnectionTestSuccess(resolveTenantOrgID(r), connectionID, summary, at)
} else {
h.recordTestSuccess(connectionID, summary, at)
}
}
writeJSON(w, http.StatusOK, map[string]any{"success": true})
return
}
writeErrorResponse(w, http.StatusNotFound, "vmware_not_found", "Connection not found", nil)
}
func (h *VMwareHandlers) writeConnectionTestResult(
w http.ResponseWriter,
r *http.Request,
instance config.VMwareVCenterInstance,
) {
_, invalidConfig, err := h.testConnectionInstance(r, instance)
if err != nil {
h.writeConnectionFailure(w, invalidConfig, err)
return
}
writeJSON(w, http.StatusOK, map[string]any{"success": true})
}
func (h *VMwareHandlers) writeConnectionFailure(w http.ResponseWriter, invalidConfig bool, err error) {
if invalidConfig {
writeErrorResponse(w, http.StatusBadRequest, "vmware_invalid_config", "Invalid VMware vCenter connection configuration", connectionFailureDetails(err))
return
}
writeErrorResponse(w, http.StatusBadRequest, "vmware_connection_failed", "Failed to connect to VMware vCenter", connectionFailureDetails(err))
}
func connectionFailureDetails(err error) map[string]string {
if err == nil {
return nil
}
details := map[string]string{"error": err.Error()}
if category := connectionErrorCategory(err); category != "" {
details["category"] = category
}
return details
}
func connectionErrorCategory(err error) string {
if err == nil {
return ""
}
if connectionErr, ok := err.(*vmware.ConnectionError); ok {
return strings.TrimSpace(connectionErr.Category)
}
return ""
}
func (h *VMwareHandlers) testConnectionInstance(
r *http.Request,
instance config.VMwareVCenterInstance,
) (*vmware.InventorySummary, bool, error) {
newClient := h.newClient
if newClient == nil {
newClient = func(cfg vmware.ClientConfig) (vmwareClient, error) { return vmware.NewClient(cfg) }
}
client, err := newClient(vmware.ClientConfig{
Host: instance.Host,
Port: instance.Port,
Username: instance.Username,
Password: instance.Password,
InsecureSkipVerify: instance.InsecureSkipVerify,
Timeout: 10 * time.Second,
})
if err != nil {
return nil, true, err
}
defer client.Close()
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
summary, err := client.TestConnection(ctx)
if err != nil {
return nil, false, err
}
return summary, false, nil
}
func normalizeVMwareInstance(instance *config.VMwareVCenterInstance) {
if instance == nil {
return
}
instance.Name = strings.TrimSpace(instance.Name)
instance.Host = strings.TrimSpace(instance.Host)
instance.Username = strings.TrimSpace(instance.Username)
instance.Password = strings.TrimSpace(instance.Password)
instance.ApplyDefaults()
}
func decodeVMwareInstanceRequest(
w http.ResponseWriter,
r *http.Request,
base config.VMwareVCenterInstance,
) (config.VMwareVCenterInstance, 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.VMwareVCenterInstance{}, false
}
return instance, true
}
func decodeOptionalVMwareInstanceRequest(
w http.ResponseWriter,
r *http.Request,
base config.VMwareVCenterInstance,
) (config.VMwareVCenterInstance, 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.VMwareVCenterInstance{}, 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.VMwareVCenterInstance{}, false, false
}
return instance, true, true
}
func (h *VMwareHandlers) featureEnabled(w http.ResponseWriter) bool {
if vmware.IsFeatureEnabled() {
return true
}
writeErrorResponse(w, http.StatusNotFound, "vmware_disabled", "VMware integration has been explicitly disabled", nil)
return false
}
func (h *VMwareHandlers) persistenceForRequest(w http.ResponseWriter, ctx context.Context) *config.ConfigPersistence {
if h == nil || h.getPersistence == nil {
writeErrorResponse(w, http.StatusInternalServerError, "vmware_unavailable", "VMware service unavailable", nil)
return nil
}
persistence := h.getPersistence(ctx)
if persistence == nil {
writeErrorResponse(w, http.StatusInternalServerError, "vmware_unavailable", "VMware service unavailable", nil)
return nil
}
return persistence
}
func (h *VMwareHandlers) pollerForRequest(ctx context.Context) *monitoring.VMwarePoller {
if h == nil || h.getPoller == nil {
return nil
}
return h.getPoller(ctx)
}
func (h *VMwareHandlers) monitorForRequest(ctx context.Context) *monitoring.Monitor {
if h == nil || h.getMonitor == nil {
return nil
}
return h.getMonitor(ctx)
}
func (h *VMwareHandlers) previewMonitoredSystemRecords(
ctx context.Context,
instance config.VMwareVCenterInstance,
) ([]unifiedresources.IngestRecord, bool, error) {
if h != nil && h.previewRecords != nil {
records, err := h.previewRecords(ctx, instance)
return records, false, err
}
client, err := vmware.NewClient(vmware.ClientConfig{
Host: instance.Host,
Port: instance.Port,
Username: instance.Username,
Password: instance.Password,
InsecureSkipVerify: instance.InsecureSkipVerify,
Timeout: 20 * time.Second,
})
if err != nil {
return nil, true, err
}
provider := vmware.NewAPIProvider(vmware.ProviderMetadata{
ConnectionID: strings.TrimSpace(instance.ID),
ConnectionName: strings.TrimSpace(instance.Name),
VCenterHost: strings.TrimSpace(instance.Host),
}, client)
defer provider.Close()
refreshCtx, cancel := context.WithTimeout(ctx, 20*time.Second)
defer cancel()
if err := provider.Refresh(refreshCtx); err != nil {
return nil, false, err
}
return provider.Records(), false, nil
}
func vmwareMonitoredSystemCandidate(
instance config.VMwareVCenterInstance,
) unifiedresources.MonitoredSystemCandidate {
return unifiedresources.MonitoredSystemCandidate{
Source: unifiedresources.SourceVMware,
Type: unifiedresources.ResourceTypeAgent,
Name: strings.TrimSpace(instance.Name),
Hostname: pulseTokenHostCandidate(instance.Host),
HostURL: strings.TrimSpace(instance.Host),
ResourceID: strings.TrimSpace(instance.ID),
State: monitoredSystemCandidateStateFromEnabled(instance.Enabled),
}
}
func (h *VMwareHandlers) runtimeStatus(connectionID string) vmwareConnectionRuntimeStatus {
if h == nil {
return vmwareConnectionRuntimeStatus{}
}
h.statusMu.RLock()
defer h.statusMu.RUnlock()
if h.statuses == nil {
return vmwareConnectionRuntimeStatus{}
}
status, ok := h.statuses[connectionID]
if !ok {
return vmwareConnectionRuntimeStatus{}
}
return cloneVMwareRuntimeStatus(status)
}
func (h *VMwareHandlers) recordTestSuccess(connectionID string, summary *vmware.InventorySummary, at time.Time) {
if h == nil {
return
}
h.statusMu.Lock()
defer h.statusMu.Unlock()
if h.statuses == nil {
h.statuses = make(map[string]vmwareConnectionRuntimeStatus)
}
current := h.statuses[connectionID]
current.Poll = &monitoring.VMwareConnectionPollStatus{
IntervalSeconds: defaultVMwarePollIntervalSeconds(),
LastAttemptAt: timePointer(at),
LastSuccessAt: timePointer(at),
}
if summary != nil {
current.Observed = &monitoring.VMwareConnectionObservedSummary{
CollectedAt: timePointer(at),
Hosts: summary.Hosts,
VMs: summary.VMs,
Datastores: summary.Datastores,
Networks: summary.Networks,
VIRelease: strings.TrimSpace(summary.VIRelease),
}
}
h.statuses[connectionID] = current
}
func (h *VMwareHandlers) recordTestFailure(connectionID string, err error, at time.Time) {
if h == nil {
return
}
h.statusMu.Lock()
defer h.statusMu.Unlock()
if h.statuses == nil {
h.statuses = make(map[string]vmwareConnectionRuntimeStatus)
}
current := h.statuses[connectionID]
poll := current.Poll
if poll == nil {
poll = &monitoring.VMwareConnectionPollStatus{
IntervalSeconds: defaultVMwarePollIntervalSeconds(),
}
}
poll.LastAttemptAt = timePointer(at)
poll.LastError = &monitoring.VMwareConnectionPollError{
At: timePointer(at),
Message: err.Error(),
Category: connectionErrorCategory(err),
}
current.Poll = poll
h.statuses[connectionID] = current
}
func (h *VMwareHandlers) clearRuntimeStatus(connectionID string) {
if h == nil {
return
}
h.statusMu.Lock()
defer h.statusMu.Unlock()
if h.statuses == nil {
return
}
delete(h.statuses, connectionID)
}
func cloneVMwareRuntimeStatus(status vmwareConnectionRuntimeStatus) vmwareConnectionRuntimeStatus {
cloned := vmwareConnectionRuntimeStatus{}
if status.Poll != nil {
poll := *status.Poll
if poll.LastAttemptAt != nil {
poll.LastAttemptAt = timePointer(*poll.LastAttemptAt)
}
if poll.LastSuccessAt != nil {
poll.LastSuccessAt = timePointer(*poll.LastSuccessAt)
}
if poll.LastError != nil {
errCopy := *poll.LastError
if errCopy.At != nil {
errCopy.At = timePointer(*errCopy.At)
}
poll.LastError = &errCopy
}
cloned.Poll = &poll
}
if status.Observed != nil {
observed := *status.Observed
if observed.CollectedAt != nil {
observed.CollectedAt = timePointer(*observed.CollectedAt)
}
cloned.Observed = &observed
}
return cloned
}
func defaultVMwarePollIntervalSeconds() int {
return int((60 * time.Second) / time.Second)
}
func timePointer(value time.Time) *time.Time {
v := value
return &v
}
func vmwareConnectionIDFromPath(path string) (string, bool) {
if !strings.HasPrefix(path, vmwareConnectionsPathPrefix) {
return "", false
}
raw := strings.Trim(strings.TrimPrefix(path, vmwareConnectionsPathPrefix), "/")
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 vmwareConnectionIDFromTestPath(path string) (string, bool) {
return vmwareConnectionIDFromActionPath(path, "/test")
}
func vmwareConnectionIDFromPreviewPath(path string) (string, bool) {
return vmwareConnectionIDFromActionPath(path, "/preview")
}
func vmwareConnectionIDFromActionPath(path string, suffix string) (string, bool) {
if !strings.HasPrefix(path, vmwareConnectionsPathPrefix) {
return "", false
}
raw := strings.Trim(strings.TrimPrefix(path, vmwareConnectionsPathPrefix), "/")
if !strings.HasSuffix(raw, suffix) {
return "", false
}
raw = strings.TrimSuffix(raw, 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
}