1269 lines
49 KiB
Go
1269 lines
49 KiB
Go
package cluster
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/jackc/pgx/v5"
|
|
|
|
"github.com/example/remote-access-platform/backend/internal/platform/authority"
|
|
"github.com/example/remote-access-platform/backend/internal/platform/httpx"
|
|
"github.com/example/remote-access-platform/backend/internal/platform/module"
|
|
"github.com/example/remote-access-platform/backend/internal/platform/secrets"
|
|
)
|
|
|
|
type Module struct {
|
|
service *Service
|
|
}
|
|
|
|
func NewModule(deps module.Dependencies, verifiers ...*authority.Verifier) *Module {
|
|
store := NewPostgresStore(deps.Infra.DB, verifiers...)
|
|
if deps.Config.Secret.EncryptionKeyBase64 != "" {
|
|
if encryptor, err := secrets.NewEncryptor(deps.Config.Secret.EncryptionKeyBase64, deps.Config.Secret.EncryptionKeyID); err == nil {
|
|
store.WithClusterKeyEncryptor(encryptor)
|
|
}
|
|
}
|
|
return &Module{service: NewService(store)}
|
|
}
|
|
|
|
func (m *Module) Name() string {
|
|
return "cluster"
|
|
}
|
|
|
|
func (m *Module) RegisterRoutes(router chi.Router) {
|
|
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}/node-groups", m.listNodeGroups)
|
|
r.Post("/{clusterID}/node-groups", m.createNodeGroup)
|
|
r.Get("/{clusterID}/join-requests", m.listJoinRequests)
|
|
r.Post("/{clusterID}/join-requests", m.createJoinRequest)
|
|
r.Post("/{clusterID}/join-requests/{requestID}/approve", m.approveJoinRequest)
|
|
r.Post("/{clusterID}/join-requests/{requestID}/reject", m.rejectJoinRequest)
|
|
r.Post("/{clusterID}/join-tokens", m.createJoinToken)
|
|
r.Post("/{clusterID}/join-tokens/{tokenID}/revoke", m.revokeJoinToken)
|
|
r.Get("/{clusterID}/nodes/{nodeID}/roles", m.listNodeRoles)
|
|
r.Post("/{clusterID}/nodes/{nodeID}/roles", m.assignNodeRole)
|
|
r.Post("/{clusterID}/nodes/{nodeID}/heartbeats", m.recordHeartbeat)
|
|
r.Get("/{clusterID}/nodes/{nodeID}/heartbeats", m.listNodeHeartbeats)
|
|
r.Get("/{clusterID}/nodes/{nodeID}/testing-flags", m.getEffectiveNodeTestingFlags)
|
|
r.Get("/{clusterID}/nodes/{nodeID}/mesh/synthetic-config", m.getNodeSyntheticMeshConfig)
|
|
r.Post("/{clusterID}/nodes/{nodeID}/telemetry", m.recordNodeTelemetry)
|
|
r.Get("/{clusterID}/nodes/{nodeID}/telemetry", m.listNodeTelemetry)
|
|
r.Post("/{clusterID}/nodes/{nodeID}/membership/attach", m.attachExistingNodeToCluster)
|
|
r.Put("/{clusterID}/nodes/{nodeID}/group", m.assignNodeGroup)
|
|
r.Post("/{clusterID}/nodes/{nodeID}/identity/revoke", m.revokeNodeIdentity)
|
|
r.Post("/{clusterID}/nodes/{nodeID}/membership/disable", m.disableMembership)
|
|
r.Get("/{clusterID}/nodes/{nodeID}/workloads/desired", m.listDesiredWorkloads)
|
|
r.Put("/{clusterID}/nodes/{nodeID}/workloads/{serviceType}/desired", m.setDesiredWorkload)
|
|
r.Post("/{clusterID}/nodes/{nodeID}/workloads/{serviceType}/status", m.reportWorkloadStatus)
|
|
r.Get("/{clusterID}/nodes/{nodeID}/workloads/status", m.listWorkloadStatuses)
|
|
r.Get("/{clusterID}/mesh/links", m.listMeshLinks)
|
|
r.Post("/{clusterID}/mesh/links", m.reportMeshLink)
|
|
r.Get("/{clusterID}/mesh/route-intents", m.listRouteIntents)
|
|
r.Post("/{clusterID}/mesh/route-intents", m.createRouteIntent)
|
|
r.Get("/{clusterID}/mesh/qos-policies", m.listQoSPolicies)
|
|
r.Get("/{clusterID}/fabric/entry-points", m.listFabricEntryPoints)
|
|
r.Post("/{clusterID}/fabric/entry-points", m.createFabricEntryPoint)
|
|
r.Get("/{clusterID}/fabric/entry-points/{entryPointID}/nodes", m.listFabricEntryPointNodes)
|
|
r.Put("/{clusterID}/fabric/entry-points/{entryPointID}/nodes/{nodeID}", m.setFabricEntryPointNode)
|
|
r.Get("/{clusterID}/fabric/egress-pools", m.listFabricEgressPools)
|
|
r.Post("/{clusterID}/fabric/egress-pools", m.createFabricEgressPool)
|
|
r.Get("/{clusterID}/fabric/egress-pools/{egressPoolID}/nodes", m.listFabricEgressPoolNodes)
|
|
r.Put("/{clusterID}/fabric/egress-pools/{egressPoolID}/nodes/{nodeID}", m.setFabricEgressPoolNode)
|
|
r.Post("/{clusterID}/vpn-connection-leases/expire-stale", m.expireStaleVPNConnectionLeases)
|
|
r.Get("/{clusterID}/vpn-connections", m.listVPNConnections)
|
|
r.Post("/{clusterID}/vpn-connections", m.createVPNConnection)
|
|
r.Get("/{clusterID}/vpn-connections/{vpnConnectionID}", m.getVPNConnection)
|
|
r.Put("/{clusterID}/vpn-connections/{vpnConnectionID}/desired-state", m.updateVPNConnectionDesiredState)
|
|
r.Get("/{clusterID}/vpn-connections/{vpnConnectionID}/allowed-nodes", m.listVPNConnectionAllowedNodes)
|
|
r.Put("/{clusterID}/vpn-connections/{vpnConnectionID}/allowed-nodes", m.setVPNConnectionAllowedNodes)
|
|
r.Get("/{clusterID}/vpn-connections/{vpnConnectionID}/route-policies", m.listVPNConnectionRoutePolicies)
|
|
r.Post("/{clusterID}/vpn-connections/{vpnConnectionID}/route-policies", m.upsertVPNConnectionRoutePolicy)
|
|
r.Get("/{clusterID}/vpn-connections/{vpnConnectionID}/leases/active", m.getActiveVPNConnectionLease)
|
|
r.Post("/{clusterID}/vpn-connections/{vpnConnectionID}/leases/acquire", m.acquireVPNConnectionLease)
|
|
r.Post("/{clusterID}/vpn-connections/{vpnConnectionID}/leases/{leaseID}/renew", m.renewVPNConnectionLease)
|
|
r.Post("/{clusterID}/vpn-connections/{vpnConnectionID}/leases/{leaseID}/release", m.releaseVPNConnectionLease)
|
|
r.Post("/{clusterID}/vpn-connections/{vpnConnectionID}/leases/{leaseID}/fence", m.fenceVPNConnectionLease)
|
|
r.Get("/{clusterID}/authority", m.getClusterAuthority)
|
|
r.Put("/{clusterID}/authority", m.updateClusterAuthority)
|
|
r.Get("/{clusterID}/audit", m.listAuditEvents)
|
|
})
|
|
router.Get("/cluster-admin-summaries", m.listClusterAdminSummaries)
|
|
router.Get("/fabric/testing-flags", m.listFabricTestingFlags)
|
|
router.Put("/fabric/testing-flags", m.upsertFabricTestingFlag)
|
|
}
|
|
|
|
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) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"clusters": items})
|
|
}
|
|
|
|
func (m *Module) getCluster(w http.ResponseWriter, r *http.Request) {
|
|
item, err := m.service.GetCluster(r.Context(), r.URL.Query().Get("actor_user_id"), chi.URLParam(r, "clusterID"))
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"cluster": item})
|
|
}
|
|
|
|
func (m *Module) createCluster(w http.ResponseWriter, r *http.Request) {
|
|
var payload struct {
|
|
ActorUserID string `json:"actor_user_id"`
|
|
Slug string `json:"slug"`
|
|
Name string `json:"name"`
|
|
Region *string `json:"region"`
|
|
Metadata json.RawMessage `json:"metadata"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
httpx.WriteError(w, http.StatusBadRequest, "invalid cluster payload")
|
|
return
|
|
}
|
|
item, err := m.service.CreateCluster(r.Context(), CreateClusterInput{
|
|
ActorUserID: payload.ActorUserID,
|
|
Slug: payload.Slug,
|
|
Name: payload.Name,
|
|
Region: payload.Region,
|
|
Metadata: payload.Metadata,
|
|
})
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusCreated, map[string]any{"cluster": item})
|
|
}
|
|
|
|
func (m *Module) updateCluster(w http.ResponseWriter, r *http.Request) {
|
|
var payload struct {
|
|
ActorUserID string `json:"actor_user_id"`
|
|
Name string `json:"name"`
|
|
Status string `json:"status"`
|
|
Region *string `json:"region"`
|
|
Metadata json.RawMessage `json:"metadata"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
httpx.WriteError(w, http.StatusBadRequest, "invalid cluster payload")
|
|
return
|
|
}
|
|
item, err := m.service.UpdateCluster(r.Context(), UpdateClusterInput{
|
|
ActorUserID: payload.ActorUserID,
|
|
ClusterID: chi.URLParam(r, "clusterID"),
|
|
Name: payload.Name,
|
|
Status: payload.Status,
|
|
Region: payload.Region,
|
|
Metadata: payload.Metadata,
|
|
})
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"cluster": item})
|
|
}
|
|
|
|
func (m *Module) listClusterNodes(w http.ResponseWriter, r *http.Request) {
|
|
items, err := m.service.ListClusterNodes(r.Context(), r.URL.Query().Get("actor_user_id"), chi.URLParam(r, "clusterID"))
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"nodes": items})
|
|
}
|
|
|
|
func (m *Module) listNodeGroups(w http.ResponseWriter, r *http.Request) {
|
|
items, err := m.service.ListNodeGroups(r.Context(), r.URL.Query().Get("actor_user_id"), chi.URLParam(r, "clusterID"))
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"node_groups": items})
|
|
}
|
|
|
|
func (m *Module) createNodeGroup(w http.ResponseWriter, r *http.Request) {
|
|
var payload struct {
|
|
ActorUserID string `json:"actor_user_id"`
|
|
ParentGroupID *string `json:"parent_group_id"`
|
|
Name string `json:"name"`
|
|
Description *string `json:"description"`
|
|
SortOrder int `json:"sort_order"`
|
|
Metadata json.RawMessage `json:"metadata"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
httpx.WriteError(w, http.StatusBadRequest, "invalid node group payload")
|
|
return
|
|
}
|
|
item, err := m.service.CreateNodeGroup(r.Context(), CreateNodeGroupInput{
|
|
ActorUserID: payload.ActorUserID,
|
|
ClusterID: chi.URLParam(r, "clusterID"),
|
|
ParentGroupID: payload.ParentGroupID,
|
|
Name: payload.Name,
|
|
Description: payload.Description,
|
|
SortOrder: payload.SortOrder,
|
|
Metadata: payload.Metadata,
|
|
})
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusCreated, map[string]any{"node_group": item})
|
|
}
|
|
|
|
func (m *Module) createJoinToken(w http.ResponseWriter, r *http.Request) {
|
|
var payload struct {
|
|
ActorUserID string `json:"actor_user_id"`
|
|
Scope json.RawMessage `json:"scope"`
|
|
ExpiresAt *time.Time `json:"expires_at"`
|
|
MaxUses int `json:"max_uses"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
httpx.WriteError(w, http.StatusBadRequest, "invalid join token payload")
|
|
return
|
|
}
|
|
expiresAt := time.Time{}
|
|
if payload.ExpiresAt != nil {
|
|
expiresAt = *payload.ExpiresAt
|
|
}
|
|
item, err := m.service.CreateJoinToken(r.Context(), CreateJoinTokenInput{
|
|
ActorUserID: payload.ActorUserID,
|
|
ClusterID: chi.URLParam(r, "clusterID"),
|
|
Scope: payload.Scope,
|
|
ExpiresAt: expiresAt,
|
|
MaxUses: payload.MaxUses,
|
|
})
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusCreated, map[string]any{"join_token": item})
|
|
}
|
|
|
|
func (m *Module) createJoinRequest(w http.ResponseWriter, r *http.Request) {
|
|
var payload struct {
|
|
JoinToken string `json:"join_token"`
|
|
NodeName string `json:"node_name"`
|
|
NodeFingerprint string `json:"node_fingerprint"`
|
|
PublicKey string `json:"public_key"`
|
|
ReportedCapabilities json.RawMessage `json:"reported_capabilities"`
|
|
ReportedFacts json.RawMessage `json:"reported_facts"`
|
|
RequestedRoles json.RawMessage `json:"requested_roles"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
httpx.WriteError(w, http.StatusBadRequest, "invalid join request payload")
|
|
return
|
|
}
|
|
item, err := m.service.CreateJoinRequest(r.Context(), CreateJoinRequestInput{
|
|
ClusterID: chi.URLParam(r, "clusterID"),
|
|
JoinToken: payload.JoinToken,
|
|
NodeName: payload.NodeName,
|
|
NodeFingerprint: payload.NodeFingerprint,
|
|
PublicKey: payload.PublicKey,
|
|
ReportedCapabilities: payload.ReportedCapabilities,
|
|
ReportedFacts: payload.ReportedFacts,
|
|
RequestedRoles: payload.RequestedRoles,
|
|
})
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusCreated, map[string]any{"join_request": item})
|
|
}
|
|
|
|
func (m *Module) listJoinRequests(w http.ResponseWriter, r *http.Request) {
|
|
items, err := m.service.ListJoinRequests(r.Context(), r.URL.Query().Get("actor_user_id"), chi.URLParam(r, "clusterID"))
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"join_requests": items})
|
|
}
|
|
|
|
func (m *Module) approveJoinRequest(w http.ResponseWriter, r *http.Request) {
|
|
var payload struct {
|
|
ActorUserID string `json:"actor_user_id"`
|
|
NodeKey string `json:"node_key"`
|
|
OwnershipType string `json:"ownership_type"`
|
|
OwnerOrganizationID *string `json:"owner_organization_id"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
httpx.WriteError(w, http.StatusBadRequest, "invalid join request approval payload")
|
|
return
|
|
}
|
|
item, err := m.service.ApproveJoinRequest(r.Context(), ApproveJoinRequestInput{
|
|
ActorUserID: payload.ActorUserID,
|
|
ClusterID: chi.URLParam(r, "clusterID"),
|
|
JoinRequestID: chi.URLParam(r, "requestID"),
|
|
NodeKey: payload.NodeKey,
|
|
OwnershipType: payload.OwnershipType,
|
|
OwnerOrganizationID: payload.OwnerOrganizationID,
|
|
})
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, item)
|
|
}
|
|
|
|
func (m *Module) rejectJoinRequest(w http.ResponseWriter, r *http.Request) {
|
|
var payload struct {
|
|
ActorUserID string `json:"actor_user_id"`
|
|
Reason string `json:"reason"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
httpx.WriteError(w, http.StatusBadRequest, "invalid join request rejection payload")
|
|
return
|
|
}
|
|
item, err := m.service.RejectJoinRequest(r.Context(), RejectJoinRequestInput{
|
|
ActorUserID: payload.ActorUserID,
|
|
ClusterID: chi.URLParam(r, "clusterID"),
|
|
JoinRequestID: chi.URLParam(r, "requestID"),
|
|
Reason: payload.Reason,
|
|
})
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"join_request": item})
|
|
}
|
|
|
|
func (m *Module) revokeJoinToken(w http.ResponseWriter, r *http.Request) {
|
|
var payload struct {
|
|
ActorUserID string `json:"actor_user_id"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
httpx.WriteError(w, http.StatusBadRequest, "invalid join token revoke payload")
|
|
return
|
|
}
|
|
item, err := m.service.RevokeJoinToken(r.Context(), RevokeJoinTokenInput{
|
|
ActorUserID: payload.ActorUserID,
|
|
ClusterID: chi.URLParam(r, "clusterID"),
|
|
TokenID: chi.URLParam(r, "tokenID"),
|
|
})
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"join_token": item})
|
|
}
|
|
|
|
func (m *Module) assignNodeRole(w http.ResponseWriter, r *http.Request) {
|
|
var payload struct {
|
|
ActorUserID string `json:"actor_user_id"`
|
|
OrganizationID *string `json:"organization_id"`
|
|
Role string `json:"role"`
|
|
Status string `json:"status"`
|
|
Policy json.RawMessage `json:"policy"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
httpx.WriteError(w, http.StatusBadRequest, "invalid node role payload")
|
|
return
|
|
}
|
|
item, err := m.service.AssignNodeRole(r.Context(), AssignNodeRoleInput{
|
|
ActorUserID: payload.ActorUserID,
|
|
ClusterID: chi.URLParam(r, "clusterID"),
|
|
NodeID: chi.URLParam(r, "nodeID"),
|
|
OrganizationID: payload.OrganizationID,
|
|
Role: payload.Role,
|
|
Status: payload.Status,
|
|
Policy: payload.Policy,
|
|
})
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusCreated, map[string]any{"role_assignment": item})
|
|
}
|
|
|
|
func (m *Module) listNodeRoles(w http.ResponseWriter, r *http.Request) {
|
|
items, err := m.service.ListNodeRoleAssignments(r.Context(), r.URL.Query().Get("actor_user_id"), chi.URLParam(r, "clusterID"), chi.URLParam(r, "nodeID"))
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"role_assignments": items})
|
|
}
|
|
|
|
func (m *Module) recordHeartbeat(w http.ResponseWriter, r *http.Request) {
|
|
var payload struct {
|
|
HealthStatus string `json:"health_status"`
|
|
ReportedVersion *string `json:"reported_version"`
|
|
Capabilities json.RawMessage `json:"capabilities"`
|
|
ServiceStates json.RawMessage `json:"service_states"`
|
|
Metadata json.RawMessage `json:"metadata"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
httpx.WriteError(w, http.StatusBadRequest, "invalid node heartbeat payload")
|
|
return
|
|
}
|
|
item, err := m.service.RecordHeartbeat(r.Context(), RecordHeartbeatInput{
|
|
ClusterID: chi.URLParam(r, "clusterID"),
|
|
NodeID: chi.URLParam(r, "nodeID"),
|
|
HealthStatus: payload.HealthStatus,
|
|
ReportedVersion: payload.ReportedVersion,
|
|
Capabilities: payload.Capabilities,
|
|
ServiceStates: payload.ServiceStates,
|
|
Metadata: payload.Metadata,
|
|
})
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
flags, _ := m.service.GetEffectiveNodeTestingFlags(r.Context(), chi.URLParam(r, "clusterID"), chi.URLParam(r, "nodeID"))
|
|
httpx.WriteJSON(w, http.StatusAccepted, map[string]any{"heartbeat": item, "testing_flags": flags})
|
|
}
|
|
|
|
func (m *Module) listNodeHeartbeats(w http.ResponseWriter, r *http.Request) {
|
|
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
|
items, err := m.service.ListNodeHeartbeats(r.Context(), r.URL.Query().Get("actor_user_id"), chi.URLParam(r, "clusterID"), chi.URLParam(r, "nodeID"), limit)
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"heartbeats": items})
|
|
}
|
|
|
|
func (m *Module) getEffectiveNodeTestingFlags(w http.ResponseWriter, r *http.Request) {
|
|
item, err := m.service.GetEffectiveNodeTestingFlags(r.Context(), chi.URLParam(r, "clusterID"), chi.URLParam(r, "nodeID"))
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"testing_flags": item})
|
|
}
|
|
|
|
func (m *Module) getNodeSyntheticMeshConfig(w http.ResponseWriter, r *http.Request) {
|
|
item, err := m.service.GetNodeSyntheticMeshConfig(r.Context(), GetNodeSyntheticMeshConfigInput{
|
|
ClusterID: chi.URLParam(r, "clusterID"),
|
|
NodeID: chi.URLParam(r, "nodeID"),
|
|
})
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"synthetic_mesh_config": item})
|
|
}
|
|
|
|
func (m *Module) recordNodeTelemetry(w http.ResponseWriter, r *http.Request) {
|
|
var payload struct {
|
|
CPUPercent *float64 `json:"cpu_percent"`
|
|
MemoryUsedBytes *int64 `json:"memory_used_bytes"`
|
|
MemoryTotalBytes *int64 `json:"memory_total_bytes"`
|
|
DiskUsedBytes *int64 `json:"disk_used_bytes"`
|
|
DiskTotalBytes *int64 `json:"disk_total_bytes"`
|
|
NetworkRxBytes *int64 `json:"network_rx_bytes"`
|
|
NetworkTxBytes *int64 `json:"network_tx_bytes"`
|
|
ProcessCount *int `json:"process_count"`
|
|
Payload json.RawMessage `json:"payload"`
|
|
ObservedAt *time.Time `json:"observed_at"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
httpx.WriteError(w, http.StatusBadRequest, "invalid node telemetry payload")
|
|
return
|
|
}
|
|
observedAt := time.Time{}
|
|
if payload.ObservedAt != nil {
|
|
observedAt = *payload.ObservedAt
|
|
}
|
|
item, err := m.service.RecordNodeTelemetry(r.Context(), RecordNodeTelemetryInput{
|
|
ClusterID: chi.URLParam(r, "clusterID"),
|
|
NodeID: chi.URLParam(r, "nodeID"),
|
|
CPUPercent: payload.CPUPercent,
|
|
MemoryUsedBytes: payload.MemoryUsedBytes,
|
|
MemoryTotalBytes: payload.MemoryTotalBytes,
|
|
DiskUsedBytes: payload.DiskUsedBytes,
|
|
DiskTotalBytes: payload.DiskTotalBytes,
|
|
NetworkRxBytes: payload.NetworkRxBytes,
|
|
NetworkTxBytes: payload.NetworkTxBytes,
|
|
ProcessCount: payload.ProcessCount,
|
|
Payload: payload.Payload,
|
|
ObservedAt: observedAt,
|
|
})
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusAccepted, map[string]any{"telemetry": item})
|
|
}
|
|
|
|
func (m *Module) listNodeTelemetry(w http.ResponseWriter, r *http.Request) {
|
|
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
|
items, err := m.service.ListNodeTelemetry(r.Context(), r.URL.Query().Get("actor_user_id"), chi.URLParam(r, "clusterID"), chi.URLParam(r, "nodeID"), limit)
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"telemetry": items})
|
|
}
|
|
|
|
func (m *Module) attachExistingNodeToCluster(w http.ResponseWriter, r *http.Request) {
|
|
var payload struct {
|
|
ActorUserID string `json:"actor_user_id"`
|
|
Roles []string `json:"roles"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
httpx.WriteError(w, http.StatusBadRequest, "invalid membership attach payload")
|
|
return
|
|
}
|
|
item, err := m.service.AttachExistingNodeToCluster(r.Context(), AttachExistingNodeInput{
|
|
ActorUserID: payload.ActorUserID,
|
|
ClusterID: chi.URLParam(r, "clusterID"),
|
|
NodeID: chi.URLParam(r, "nodeID"),
|
|
Roles: payload.Roles,
|
|
})
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"node": item})
|
|
}
|
|
|
|
func (m *Module) assignNodeGroup(w http.ResponseWriter, r *http.Request) {
|
|
var payload struct {
|
|
ActorUserID string `json:"actor_user_id"`
|
|
GroupID *string `json:"group_id"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
httpx.WriteError(w, http.StatusBadRequest, "invalid node group assignment payload")
|
|
return
|
|
}
|
|
item, err := m.service.AssignNodeToGroup(r.Context(), AssignNodeGroupInput{
|
|
ActorUserID: payload.ActorUserID,
|
|
ClusterID: chi.URLParam(r, "clusterID"),
|
|
NodeID: chi.URLParam(r, "nodeID"),
|
|
GroupID: payload.GroupID,
|
|
})
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"node": item})
|
|
}
|
|
|
|
func (m *Module) revokeNodeIdentity(w http.ResponseWriter, r *http.Request) {
|
|
var payload struct {
|
|
ActorUserID string `json:"actor_user_id"`
|
|
Reason string `json:"reason"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
httpx.WriteError(w, http.StatusBadRequest, "invalid node identity revoke payload")
|
|
return
|
|
}
|
|
err := m.service.RevokeNodeIdentity(r.Context(), RevokeNodeIdentityInput{
|
|
ActorUserID: payload.ActorUserID,
|
|
ClusterID: chi.URLParam(r, "clusterID"),
|
|
NodeID: chi.URLParam(r, "nodeID"),
|
|
Reason: payload.Reason,
|
|
})
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusAccepted, map[string]any{"status": "accepted"})
|
|
}
|
|
|
|
func (m *Module) disableMembership(w http.ResponseWriter, r *http.Request) {
|
|
var payload struct {
|
|
ActorUserID string `json:"actor_user_id"`
|
|
Reason string `json:"reason"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
httpx.WriteError(w, http.StatusBadRequest, "invalid membership disable payload")
|
|
return
|
|
}
|
|
err := m.service.DisableClusterMembership(r.Context(), DisableMembershipInput{
|
|
ActorUserID: payload.ActorUserID,
|
|
ClusterID: chi.URLParam(r, "clusterID"),
|
|
NodeID: chi.URLParam(r, "nodeID"),
|
|
Reason: payload.Reason,
|
|
})
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusAccepted, map[string]any{"status": "accepted"})
|
|
}
|
|
|
|
func (m *Module) setDesiredWorkload(w http.ResponseWriter, r *http.Request) {
|
|
var payload struct {
|
|
ActorUserID string `json:"actor_user_id"`
|
|
DesiredState string `json:"desired_state"`
|
|
Version *string `json:"version"`
|
|
RuntimeMode string `json:"runtime_mode"`
|
|
ArtifactRef *string `json:"artifact_ref"`
|
|
Config json.RawMessage `json:"config"`
|
|
Environment json.RawMessage `json:"environment"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
httpx.WriteError(w, http.StatusBadRequest, "invalid desired workload payload")
|
|
return
|
|
}
|
|
item, err := m.service.SetDesiredWorkload(r.Context(), SetDesiredWorkloadInput{
|
|
ActorUserID: payload.ActorUserID,
|
|
ClusterID: chi.URLParam(r, "clusterID"),
|
|
NodeID: chi.URLParam(r, "nodeID"),
|
|
ServiceType: chi.URLParam(r, "serviceType"),
|
|
DesiredState: payload.DesiredState,
|
|
Version: payload.Version,
|
|
RuntimeMode: payload.RuntimeMode,
|
|
ArtifactRef: payload.ArtifactRef,
|
|
Config: payload.Config,
|
|
Environment: payload.Environment,
|
|
})
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"desired_workload": item})
|
|
}
|
|
|
|
func (m *Module) listDesiredWorkloads(w http.ResponseWriter, r *http.Request) {
|
|
items, err := m.service.ListDesiredWorkloads(r.Context(), r.URL.Query().Get("actor_user_id"), chi.URLParam(r, "clusterID"), chi.URLParam(r, "nodeID"))
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"desired_workloads": items})
|
|
}
|
|
|
|
func (m *Module) reportWorkloadStatus(w http.ResponseWriter, r *http.Request) {
|
|
var payload struct {
|
|
ReportedState string `json:"reported_state"`
|
|
RuntimeMode string `json:"runtime_mode"`
|
|
Version *string `json:"version"`
|
|
StatusPayload json.RawMessage `json:"status_payload"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
httpx.WriteError(w, http.StatusBadRequest, "invalid workload status payload")
|
|
return
|
|
}
|
|
item, err := m.service.ReportWorkloadStatus(r.Context(), ReportWorkloadStatusInput{
|
|
ClusterID: chi.URLParam(r, "clusterID"),
|
|
NodeID: chi.URLParam(r, "nodeID"),
|
|
ServiceType: chi.URLParam(r, "serviceType"),
|
|
ReportedState: payload.ReportedState,
|
|
RuntimeMode: payload.RuntimeMode,
|
|
Version: payload.Version,
|
|
StatusPayload: payload.StatusPayload,
|
|
})
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusAccepted, map[string]any{"workload_status": item})
|
|
}
|
|
|
|
func (m *Module) listWorkloadStatuses(w http.ResponseWriter, r *http.Request) {
|
|
items, err := m.service.ListLatestWorkloadStatuses(r.Context(), r.URL.Query().Get("actor_user_id"), chi.URLParam(r, "clusterID"), chi.URLParam(r, "nodeID"))
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"workload_statuses": items})
|
|
}
|
|
|
|
func (m *Module) reportMeshLink(w http.ResponseWriter, r *http.Request) {
|
|
var payload struct {
|
|
SourceNodeID string `json:"source_node_id"`
|
|
TargetNodeID string `json:"target_node_id"`
|
|
LinkStatus string `json:"link_status"`
|
|
LatencyMs *int `json:"latency_ms"`
|
|
QualityScore *int `json:"quality_score"`
|
|
Metadata json.RawMessage `json:"metadata"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
httpx.WriteError(w, http.StatusBadRequest, "invalid mesh link payload")
|
|
return
|
|
}
|
|
item, err := m.service.ReportMeshLink(r.Context(), ReportMeshLinkInput{
|
|
ClusterID: chi.URLParam(r, "clusterID"),
|
|
SourceNodeID: payload.SourceNodeID,
|
|
TargetNodeID: payload.TargetNodeID,
|
|
LinkStatus: payload.LinkStatus,
|
|
LatencyMs: payload.LatencyMs,
|
|
QualityScore: payload.QualityScore,
|
|
Metadata: payload.Metadata,
|
|
})
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusAccepted, map[string]any{"mesh_link": item})
|
|
}
|
|
|
|
func (m *Module) listMeshLinks(w http.ResponseWriter, r *http.Request) {
|
|
items, err := m.service.ListMeshLinks(r.Context(), r.URL.Query().Get("actor_user_id"), chi.URLParam(r, "clusterID"))
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"mesh_links": items})
|
|
}
|
|
|
|
func (m *Module) createRouteIntent(w http.ResponseWriter, r *http.Request) {
|
|
var payload struct {
|
|
ActorUserID string `json:"actor_user_id"`
|
|
SourceSelector json.RawMessage `json:"source_selector"`
|
|
DestinationSelector json.RawMessage `json:"destination_selector"`
|
|
ServiceClass string `json:"service_class"`
|
|
Priority int `json:"priority"`
|
|
Policy json.RawMessage `json:"policy"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
httpx.WriteError(w, http.StatusBadRequest, "invalid route intent payload")
|
|
return
|
|
}
|
|
item, err := m.service.CreateRouteIntent(r.Context(), CreateRouteIntentInput{
|
|
ActorUserID: payload.ActorUserID,
|
|
ClusterID: chi.URLParam(r, "clusterID"),
|
|
SourceSelector: payload.SourceSelector,
|
|
DestinationSelector: payload.DestinationSelector,
|
|
ServiceClass: payload.ServiceClass,
|
|
Priority: payload.Priority,
|
|
Policy: payload.Policy,
|
|
})
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusCreated, map[string]any{"route_intent": item})
|
|
}
|
|
|
|
func (m *Module) listRouteIntents(w http.ResponseWriter, r *http.Request) {
|
|
items, err := m.service.ListRouteIntents(r.Context(), r.URL.Query().Get("actor_user_id"), chi.URLParam(r, "clusterID"))
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"route_intents": items})
|
|
}
|
|
|
|
func (m *Module) listQoSPolicies(w http.ResponseWriter, r *http.Request) {
|
|
items, err := m.service.ListQoSPolicies(r.Context(), r.URL.Query().Get("actor_user_id"), chi.URLParam(r, "clusterID"))
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"qos_policies": items})
|
|
}
|
|
|
|
func (m *Module) listFabricEntryPoints(w http.ResponseWriter, r *http.Request) {
|
|
items, err := m.service.ListFabricEntryPoints(r.Context(), r.URL.Query().Get("actor_user_id"), chi.URLParam(r, "clusterID"))
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"entry_points": items})
|
|
}
|
|
|
|
func (m *Module) createFabricEntryPoint(w http.ResponseWriter, r *http.Request) {
|
|
var payload struct {
|
|
ActorUserID string `json:"actor_user_id"`
|
|
Name string `json:"name"`
|
|
Status string `json:"status"`
|
|
EndpointType string `json:"endpoint_type"`
|
|
PublicEndpoint *string `json:"public_endpoint"`
|
|
Policy json.RawMessage `json:"policy"`
|
|
Metadata json.RawMessage `json:"metadata"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
httpx.WriteError(w, http.StatusBadRequest, "invalid fabric entry point payload")
|
|
return
|
|
}
|
|
item, err := m.service.CreateFabricEntryPoint(r.Context(), CreateFabricEntryPointInput{
|
|
ActorUserID: payload.ActorUserID,
|
|
ClusterID: chi.URLParam(r, "clusterID"),
|
|
Name: payload.Name,
|
|
Status: payload.Status,
|
|
EndpointType: payload.EndpointType,
|
|
PublicEndpoint: payload.PublicEndpoint,
|
|
Policy: payload.Policy,
|
|
Metadata: payload.Metadata,
|
|
})
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusCreated, map[string]any{"entry_point": item})
|
|
}
|
|
|
|
func (m *Module) setFabricEntryPointNode(w http.ResponseWriter, r *http.Request) {
|
|
var payload struct {
|
|
ActorUserID string `json:"actor_user_id"`
|
|
Status string `json:"status"`
|
|
Priority int `json:"priority"`
|
|
Metadata json.RawMessage `json:"metadata"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
httpx.WriteError(w, http.StatusBadRequest, "invalid fabric entry point node payload")
|
|
return
|
|
}
|
|
item, err := m.service.SetFabricEntryPointNode(r.Context(), SetFabricEntryPointNodeInput{
|
|
ActorUserID: payload.ActorUserID,
|
|
ClusterID: chi.URLParam(r, "clusterID"),
|
|
EntryPointID: chi.URLParam(r, "entryPointID"),
|
|
NodeID: chi.URLParam(r, "nodeID"),
|
|
Status: payload.Status,
|
|
Priority: payload.Priority,
|
|
Metadata: payload.Metadata,
|
|
})
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"entry_point_node": item})
|
|
}
|
|
|
|
func (m *Module) listFabricEntryPointNodes(w http.ResponseWriter, r *http.Request) {
|
|
items, err := m.service.ListFabricEntryPointNodes(
|
|
r.Context(),
|
|
r.URL.Query().Get("actor_user_id"),
|
|
chi.URLParam(r, "clusterID"),
|
|
chi.URLParam(r, "entryPointID"),
|
|
)
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"entry_point_nodes": items})
|
|
}
|
|
|
|
func (m *Module) listFabricEgressPools(w http.ResponseWriter, r *http.Request) {
|
|
items, err := m.service.ListFabricEgressPools(r.Context(), r.URL.Query().Get("actor_user_id"), chi.URLParam(r, "clusterID"))
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"egress_pools": items})
|
|
}
|
|
|
|
func (m *Module) createFabricEgressPool(w http.ResponseWriter, r *http.Request) {
|
|
var payload struct {
|
|
ActorUserID string `json:"actor_user_id"`
|
|
Name string `json:"name"`
|
|
Status string `json:"status"`
|
|
Description *string `json:"description"`
|
|
RouteScope json.RawMessage `json:"route_scope"`
|
|
Policy json.RawMessage `json:"policy"`
|
|
Metadata json.RawMessage `json:"metadata"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
httpx.WriteError(w, http.StatusBadRequest, "invalid fabric egress pool payload")
|
|
return
|
|
}
|
|
item, err := m.service.CreateFabricEgressPool(r.Context(), CreateFabricEgressPoolInput{
|
|
ActorUserID: payload.ActorUserID,
|
|
ClusterID: chi.URLParam(r, "clusterID"),
|
|
Name: payload.Name,
|
|
Status: payload.Status,
|
|
Description: payload.Description,
|
|
RouteScope: payload.RouteScope,
|
|
Policy: payload.Policy,
|
|
Metadata: payload.Metadata,
|
|
})
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusCreated, map[string]any{"egress_pool": item})
|
|
}
|
|
|
|
func (m *Module) setFabricEgressPoolNode(w http.ResponseWriter, r *http.Request) {
|
|
var payload struct {
|
|
ActorUserID string `json:"actor_user_id"`
|
|
Status string `json:"status"`
|
|
Priority int `json:"priority"`
|
|
Metadata json.RawMessage `json:"metadata"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
httpx.WriteError(w, http.StatusBadRequest, "invalid fabric egress pool node payload")
|
|
return
|
|
}
|
|
item, err := m.service.SetFabricEgressPoolNode(r.Context(), SetFabricEgressPoolNodeInput{
|
|
ActorUserID: payload.ActorUserID,
|
|
ClusterID: chi.URLParam(r, "clusterID"),
|
|
EgressPoolID: chi.URLParam(r, "egressPoolID"),
|
|
NodeID: chi.URLParam(r, "nodeID"),
|
|
Status: payload.Status,
|
|
Priority: payload.Priority,
|
|
Metadata: payload.Metadata,
|
|
})
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"egress_pool_node": item})
|
|
}
|
|
|
|
func (m *Module) listFabricEgressPoolNodes(w http.ResponseWriter, r *http.Request) {
|
|
items, err := m.service.ListFabricEgressPoolNodes(
|
|
r.Context(),
|
|
r.URL.Query().Get("actor_user_id"),
|
|
chi.URLParam(r, "clusterID"),
|
|
chi.URLParam(r, "egressPoolID"),
|
|
)
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"egress_pool_nodes": items})
|
|
}
|
|
|
|
func (m *Module) createVPNConnection(w http.ResponseWriter, r *http.Request) {
|
|
var payload struct {
|
|
ActorUserID string `json:"actor_user_id"`
|
|
OrganizationID string `json:"organization_id"`
|
|
Name string `json:"name"`
|
|
TargetEndpoint json.RawMessage `json:"target_endpoint"`
|
|
ProtocolFamily string `json:"protocol_family"`
|
|
CredentialRef *string `json:"credential_ref"`
|
|
Mode string `json:"mode"`
|
|
DesiredState string `json:"desired_state"`
|
|
AllowedNodePolicy json.RawMessage `json:"allowed_node_policy"`
|
|
RoutingUsage json.RawMessage `json:"routing_usage"`
|
|
RoutePolicy json.RawMessage `json:"route_policy"`
|
|
QoSPolicy json.RawMessage `json:"qos_policy"`
|
|
PlacementPolicy json.RawMessage `json:"placement_policy"`
|
|
Metadata json.RawMessage `json:"metadata"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
httpx.WriteError(w, http.StatusBadRequest, "invalid vpn connection payload")
|
|
return
|
|
}
|
|
item, err := m.service.CreateVPNConnection(r.Context(), CreateVPNConnectionInput{
|
|
ActorUserID: payload.ActorUserID,
|
|
ClusterID: chi.URLParam(r, "clusterID"),
|
|
OrganizationID: payload.OrganizationID,
|
|
Name: payload.Name,
|
|
TargetEndpoint: payload.TargetEndpoint,
|
|
ProtocolFamily: payload.ProtocolFamily,
|
|
CredentialRef: payload.CredentialRef,
|
|
Mode: payload.Mode,
|
|
DesiredState: payload.DesiredState,
|
|
AllowedNodePolicy: payload.AllowedNodePolicy,
|
|
RoutingUsage: payload.RoutingUsage,
|
|
RoutePolicy: payload.RoutePolicy,
|
|
QoSPolicy: payload.QoSPolicy,
|
|
PlacementPolicy: payload.PlacementPolicy,
|
|
Metadata: payload.Metadata,
|
|
})
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusCreated, map[string]any{"vpn_connection": item})
|
|
}
|
|
|
|
func (m *Module) listVPNConnections(w http.ResponseWriter, r *http.Request) {
|
|
items, err := m.service.ListVPNConnections(r.Context(), r.URL.Query().Get("actor_user_id"), chi.URLParam(r, "clusterID"))
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"vpn_connections": items})
|
|
}
|
|
|
|
func (m *Module) getVPNConnection(w http.ResponseWriter, r *http.Request) {
|
|
item, err := m.service.GetVPNConnection(r.Context(), r.URL.Query().Get("actor_user_id"), chi.URLParam(r, "clusterID"), chi.URLParam(r, "vpnConnectionID"))
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"vpn_connection": item})
|
|
}
|
|
|
|
func (m *Module) updateVPNConnectionDesiredState(w http.ResponseWriter, r *http.Request) {
|
|
var payload struct {
|
|
ActorUserID string `json:"actor_user_id"`
|
|
DesiredState string `json:"desired_state"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
httpx.WriteError(w, http.StatusBadRequest, "invalid vpn connection desired state payload")
|
|
return
|
|
}
|
|
item, err := m.service.UpdateVPNConnectionDesiredState(r.Context(), UpdateVPNConnectionDesiredStateInput{
|
|
ActorUserID: payload.ActorUserID,
|
|
ClusterID: chi.URLParam(r, "clusterID"),
|
|
VPNConnectionID: chi.URLParam(r, "vpnConnectionID"),
|
|
DesiredState: payload.DesiredState,
|
|
})
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"vpn_connection": item})
|
|
}
|
|
|
|
func (m *Module) upsertVPNConnectionRoutePolicy(w http.ResponseWriter, r *http.Request) {
|
|
var payload struct {
|
|
ActorUserID string `json:"actor_user_id"`
|
|
RouteType string `json:"route_type"`
|
|
Destination string `json:"destination"`
|
|
Action string `json:"action"`
|
|
ServiceType *string `json:"service_type"`
|
|
Priority int `json:"priority"`
|
|
Policy json.RawMessage `json:"policy"`
|
|
Status string `json:"status"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
httpx.WriteError(w, http.StatusBadRequest, "invalid vpn route policy payload")
|
|
return
|
|
}
|
|
item, err := m.service.UpsertVPNConnectionRoutePolicy(r.Context(), UpsertVPNConnectionRoutePolicyInput{
|
|
ActorUserID: payload.ActorUserID,
|
|
ClusterID: chi.URLParam(r, "clusterID"),
|
|
VPNConnectionID: chi.URLParam(r, "vpnConnectionID"),
|
|
RouteType: payload.RouteType,
|
|
Destination: payload.Destination,
|
|
Action: payload.Action,
|
|
ServiceType: payload.ServiceType,
|
|
Priority: payload.Priority,
|
|
Policy: payload.Policy,
|
|
Status: payload.Status,
|
|
})
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusCreated, map[string]any{"route_policy": item})
|
|
}
|
|
|
|
func (m *Module) listVPNConnectionRoutePolicies(w http.ResponseWriter, r *http.Request) {
|
|
items, err := m.service.ListVPNConnectionRoutePolicies(r.Context(), r.URL.Query().Get("actor_user_id"), chi.URLParam(r, "clusterID"), chi.URLParam(r, "vpnConnectionID"))
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"route_policies": items})
|
|
}
|
|
|
|
func (m *Module) setVPNConnectionAllowedNodes(w http.ResponseWriter, r *http.Request) {
|
|
var payload struct {
|
|
ActorUserID string `json:"actor_user_id"`
|
|
NodeIDs []string `json:"node_ids"`
|
|
RolePreference string `json:"role_preference"`
|
|
Metadata json.RawMessage `json:"metadata"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
httpx.WriteError(w, http.StatusBadRequest, "invalid vpn allowed nodes payload")
|
|
return
|
|
}
|
|
items, err := m.service.SetVPNConnectionAllowedNodes(r.Context(), SetVPNConnectionAllowedNodesInput{
|
|
ActorUserID: payload.ActorUserID,
|
|
ClusterID: chi.URLParam(r, "clusterID"),
|
|
VPNConnectionID: chi.URLParam(r, "vpnConnectionID"),
|
|
NodeIDs: payload.NodeIDs,
|
|
RolePreference: payload.RolePreference,
|
|
Metadata: payload.Metadata,
|
|
})
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"allowed_nodes": items})
|
|
}
|
|
|
|
func (m *Module) listVPNConnectionAllowedNodes(w http.ResponseWriter, r *http.Request) {
|
|
items, err := m.service.ListVPNConnectionAllowedNodes(r.Context(), r.URL.Query().Get("actor_user_id"), chi.URLParam(r, "clusterID"), chi.URLParam(r, "vpnConnectionID"))
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"allowed_nodes": items})
|
|
}
|
|
|
|
func (m *Module) acquireVPNConnectionLease(w http.ResponseWriter, r *http.Request) {
|
|
var payload struct {
|
|
ActorUserID string `json:"actor_user_id"`
|
|
OwnerNodeID string `json:"owner_node_id"`
|
|
TTLSeconds int `json:"ttl_seconds"`
|
|
Metadata json.RawMessage `json:"metadata"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
httpx.WriteError(w, http.StatusBadRequest, "invalid vpn lease acquire payload")
|
|
return
|
|
}
|
|
item, err := m.service.AcquireVPNConnectionLease(r.Context(), AcquireVPNConnectionLeaseInput{
|
|
ActorUserID: payload.ActorUserID,
|
|
ClusterID: chi.URLParam(r, "clusterID"),
|
|
VPNConnectionID: chi.URLParam(r, "vpnConnectionID"),
|
|
OwnerNodeID: payload.OwnerNodeID,
|
|
TTL: time.Duration(payload.TTLSeconds) * time.Second,
|
|
Metadata: payload.Metadata,
|
|
})
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusCreated, map[string]any{"lease": item})
|
|
}
|
|
|
|
func (m *Module) renewVPNConnectionLease(w http.ResponseWriter, r *http.Request) {
|
|
var payload struct {
|
|
ActorUserID string `json:"actor_user_id"`
|
|
OwnerNodeID string `json:"owner_node_id"`
|
|
FencingToken string `json:"fencing_token"`
|
|
TTLSeconds int `json:"ttl_seconds"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
httpx.WriteError(w, http.StatusBadRequest, "invalid vpn lease renew payload")
|
|
return
|
|
}
|
|
item, err := m.service.RenewVPNConnectionLease(r.Context(), RenewVPNConnectionLeaseInput{
|
|
ActorUserID: payload.ActorUserID,
|
|
ClusterID: chi.URLParam(r, "clusterID"),
|
|
VPNConnectionID: chi.URLParam(r, "vpnConnectionID"),
|
|
LeaseID: chi.URLParam(r, "leaseID"),
|
|
OwnerNodeID: payload.OwnerNodeID,
|
|
FencingToken: payload.FencingToken,
|
|
TTL: time.Duration(payload.TTLSeconds) * time.Second,
|
|
})
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"lease": item})
|
|
}
|
|
|
|
func (m *Module) releaseVPNConnectionLease(w http.ResponseWriter, r *http.Request) {
|
|
var payload struct {
|
|
ActorUserID string `json:"actor_user_id"`
|
|
OwnerNodeID string `json:"owner_node_id"`
|
|
FencingToken string `json:"fencing_token"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
httpx.WriteError(w, http.StatusBadRequest, "invalid vpn lease release payload")
|
|
return
|
|
}
|
|
item, err := m.service.ReleaseVPNConnectionLease(r.Context(), ReleaseVPNConnectionLeaseInput{
|
|
ActorUserID: payload.ActorUserID,
|
|
ClusterID: chi.URLParam(r, "clusterID"),
|
|
VPNConnectionID: chi.URLParam(r, "vpnConnectionID"),
|
|
LeaseID: chi.URLParam(r, "leaseID"),
|
|
OwnerNodeID: payload.OwnerNodeID,
|
|
FencingToken: payload.FencingToken,
|
|
})
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"lease": item})
|
|
}
|
|
|
|
func (m *Module) fenceVPNConnectionLease(w http.ResponseWriter, r *http.Request) {
|
|
var payload struct {
|
|
ActorUserID string `json:"actor_user_id"`
|
|
Reason string `json:"reason"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
httpx.WriteError(w, http.StatusBadRequest, "invalid vpn lease fence payload")
|
|
return
|
|
}
|
|
item, err := m.service.FenceVPNConnectionLease(r.Context(), FenceVPNConnectionLeaseInput{
|
|
ActorUserID: payload.ActorUserID,
|
|
ClusterID: chi.URLParam(r, "clusterID"),
|
|
VPNConnectionID: chi.URLParam(r, "vpnConnectionID"),
|
|
LeaseID: chi.URLParam(r, "leaseID"),
|
|
Reason: payload.Reason,
|
|
})
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"lease": item})
|
|
}
|
|
|
|
func (m *Module) getActiveVPNConnectionLease(w http.ResponseWriter, r *http.Request) {
|
|
item, err := m.service.GetActiveVPNConnectionLease(r.Context(), r.URL.Query().Get("actor_user_id"), chi.URLParam(r, "clusterID"), chi.URLParam(r, "vpnConnectionID"))
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"lease": item})
|
|
}
|
|
|
|
func (m *Module) expireStaleVPNConnectionLeases(w http.ResponseWriter, r *http.Request) {
|
|
var payload struct {
|
|
ActorUserID string `json:"actor_user_id"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
httpx.WriteError(w, http.StatusBadRequest, "invalid vpn stale lease payload")
|
|
return
|
|
}
|
|
items, err := m.service.ExpireStaleVPNConnectionLeases(r.Context(), ExpireStaleVPNConnectionLeasesInput{
|
|
ActorUserID: payload.ActorUserID,
|
|
ClusterID: chi.URLParam(r, "clusterID"),
|
|
})
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"expired_leases": items})
|
|
}
|
|
|
|
func (m *Module) getClusterAuthority(w http.ResponseWriter, r *http.Request) {
|
|
item, err := m.service.GetClusterAuthorityState(r.Context(), r.URL.Query().Get("actor_user_id"), chi.URLParam(r, "clusterID"))
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"authority_state": item})
|
|
}
|
|
|
|
func (m *Module) updateClusterAuthority(w http.ResponseWriter, r *http.Request) {
|
|
var payload struct {
|
|
ActorUserID string `json:"actor_user_id"`
|
|
AuthorityState string `json:"authority_state"`
|
|
MutationMode string `json:"mutation_mode"`
|
|
Notes *string `json:"notes"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
httpx.WriteError(w, http.StatusBadRequest, "invalid cluster authority payload")
|
|
return
|
|
}
|
|
item, err := m.service.UpdateClusterAuthorityState(r.Context(), UpdateClusterAuthorityInput{
|
|
ActorUserID: payload.ActorUserID,
|
|
ClusterID: chi.URLParam(r, "clusterID"),
|
|
AuthorityState: payload.AuthorityState,
|
|
MutationMode: payload.MutationMode,
|
|
Notes: payload.Notes,
|
|
})
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"authority_state": item})
|
|
}
|
|
|
|
func (m *Module) listClusterAdminSummaries(w http.ResponseWriter, r *http.Request) {
|
|
items, err := m.service.ListClusterAdminSummaries(r.Context(), r.URL.Query().Get("actor_user_id"))
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"cluster_summaries": items})
|
|
}
|
|
|
|
func (m *Module) listFabricTestingFlags(w http.ResponseWriter, r *http.Request) {
|
|
items, err := m.service.ListFabricTestingFlags(r.Context(), r.URL.Query().Get("actor_user_id"))
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"testing_flags": items})
|
|
}
|
|
|
|
func (m *Module) upsertFabricTestingFlag(w http.ResponseWriter, r *http.Request) {
|
|
var payload struct {
|
|
ActorUserID string `json:"actor_user_id"`
|
|
ScopeType string `json:"scope_type"`
|
|
ScopeID *string `json:"scope_id"`
|
|
ClusterID *string `json:"cluster_id"`
|
|
Enabled bool `json:"enabled"`
|
|
TelemetryEnabled bool `json:"telemetry_enabled"`
|
|
SyntheticLinksEnabled bool `json:"synthetic_links_enabled"`
|
|
HistoryRetentionHours int `json:"history_retention_hours"`
|
|
Metadata json.RawMessage `json:"metadata"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
httpx.WriteError(w, http.StatusBadRequest, "invalid testing flag payload")
|
|
return
|
|
}
|
|
item, err := m.service.UpsertFabricTestingFlag(r.Context(), UpsertFabricTestingFlagInput{
|
|
ActorUserID: payload.ActorUserID,
|
|
ScopeType: payload.ScopeType,
|
|
ScopeID: payload.ScopeID,
|
|
ClusterID: payload.ClusterID,
|
|
Enabled: payload.Enabled,
|
|
TelemetryEnabled: payload.TelemetryEnabled,
|
|
SyntheticLinksEnabled: payload.SyntheticLinksEnabled,
|
|
HistoryRetentionHours: payload.HistoryRetentionHours,
|
|
Metadata: payload.Metadata,
|
|
})
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"testing_flag": item})
|
|
}
|
|
|
|
func (m *Module) listAuditEvents(w http.ResponseWriter, r *http.Request) {
|
|
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
|
items, err := m.service.ListAuditEvents(r.Context(), r.URL.Query().Get("actor_user_id"), chi.URLParam(r, "clusterID"), limit)
|
|
if writeServiceError(w, err) {
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"audit_events": items})
|
|
}
|
|
|
|
func writeServiceError(w http.ResponseWriter, err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
switch {
|
|
case errors.Is(err, ErrAccessDenied):
|
|
httpx.WriteError(w, http.StatusForbidden, err.Error())
|
|
case errors.Is(err, ErrVPNLeaseOwnerNotAllowed), errors.Is(err, ErrVPNLeaseOwnerRoleRequired):
|
|
httpx.WriteError(w, http.StatusForbidden, err.Error())
|
|
case errors.Is(err, ErrClusterReadOnly):
|
|
httpx.WriteError(w, http.StatusConflict, err.Error())
|
|
case errors.Is(err, ErrVPNLeaseAlreadyActive):
|
|
httpx.WriteError(w, http.StatusConflict, err.Error())
|
|
case errors.Is(err, ErrInvalidPayload), errors.Is(err, ErrInvalidJoinToken), errors.Is(err, ErrInvalidNodeRole):
|
|
httpx.WriteError(w, http.StatusBadRequest, err.Error())
|
|
case errors.Is(err, ErrInvalidCluster), errors.Is(err, ErrInvalidJoinRequest), errors.Is(err, ErrInvalidVPNConnection), errors.Is(err, ErrInvalidVPNLease), errors.Is(err, pgx.ErrNoRows):
|
|
httpx.WriteError(w, http.StatusNotFound, err.Error())
|
|
default:
|
|
httpx.WriteError(w, http.StatusInternalServerError, err.Error())
|
|
}
|
|
return true
|
|
}
|