рабочий вариант, но скороть 10 МБит
This commit is contained in:
@@ -2,13 +2,9 @@ package nodeagent
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
clustermodule "github.com/example/remote-access-platform/backend/internal/modules/cluster"
|
||||
"github.com/example/remote-access-platform/backend/internal/platform/httpx"
|
||||
@@ -17,7 +13,6 @@ import (
|
||||
)
|
||||
|
||||
type Module struct {
|
||||
db *pgxpool.Pool
|
||||
cluster *clustermodule.Service
|
||||
}
|
||||
|
||||
@@ -29,7 +24,6 @@ func NewModule(deps module.Dependencies) *Module {
|
||||
}
|
||||
}
|
||||
return &Module{
|
||||
db: deps.Infra.DB,
|
||||
cluster: clustermodule.NewService(clusterStore),
|
||||
}
|
||||
}
|
||||
@@ -40,62 +34,89 @@ func (m *Module) Name() string {
|
||||
|
||||
func (m *Module) RegisterRoutes(router chi.Router) {
|
||||
router.Route("/node-agents", func(r chi.Router) {
|
||||
r.Post("/docker-install-profile", m.dockerInstallProfile)
|
||||
r.Post("/windows-install-profile", m.windowsInstallProfile)
|
||||
r.Post("/linux-install-profile", m.linuxInstallProfile)
|
||||
r.Post("/docker-join-bundle", m.dockerJoinBundle)
|
||||
r.Post("/windows-join-bundle", m.windowsJoinBundle)
|
||||
r.Post("/linux-join-bundle", m.linuxJoinBundle)
|
||||
r.Post("/register", m.registerFabricNode)
|
||||
r.Post("/enroll", m.enrollAgent)
|
||||
r.Post("/enrollments/{requestID}/bootstrap", m.bootstrapEnrollment)
|
||||
r.Post("/register", m.registerAgent)
|
||||
r.Post("/{nodeID}/health", m.reportHealth)
|
||||
r.Post("/{nodeID}/services/status", m.reportServiceStatus)
|
||||
r.Post("/{nodeID}/update-manifest/request", m.requestUpdateManifest)
|
||||
r.Post("/{nodeID}/update-result", m.acknowledgeUpdateResult)
|
||||
r.Post("/{nodeID}/rollback-result", m.reportRollbackResult)
|
||||
r.Get("/{nodeID}/clusters/{clusterID}/vpn-assignments/desired", m.listVPNAssignments)
|
||||
r.Post("/{nodeID}/clusters/{clusterID}/vpn-assignments/{vpnConnectionID}/status", m.reportVPNAssignmentStatus)
|
||||
r.Post("/enrollments/{requestID}/join", m.fetchEnrollmentJoinContract)
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Module) linuxInstallProfile(w http.ResponseWriter, r *http.Request) {
|
||||
func (m *Module) linuxJoinBundle(w http.ResponseWriter, r *http.Request) {
|
||||
var payload clustermodule.DockerInstallProfileRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
httpx.WriteError(w, http.StatusBadRequest, "invalid linux install profile payload")
|
||||
httpx.WriteError(w, http.StatusBadRequest, "invalid linux join bundle payload")
|
||||
return
|
||||
}
|
||||
profile, err := m.cluster.GetLinuxInstallProfile(r.Context(), payload)
|
||||
bundle, err := m.cluster.GetLinuxJoinBundle(r.Context(), payload)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"linux_install_profile": profile})
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"join_bundle": bundle})
|
||||
}
|
||||
|
||||
func (m *Module) windowsInstallProfile(w http.ResponseWriter, r *http.Request) {
|
||||
func (m *Module) windowsJoinBundle(w http.ResponseWriter, r *http.Request) {
|
||||
var payload clustermodule.DockerInstallProfileRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
httpx.WriteError(w, http.StatusBadRequest, "invalid windows install profile payload")
|
||||
httpx.WriteError(w, http.StatusBadRequest, "invalid windows join bundle payload")
|
||||
return
|
||||
}
|
||||
profile, err := m.cluster.GetWindowsInstallProfile(r.Context(), payload)
|
||||
bundle, err := m.cluster.GetWindowsJoinBundle(r.Context(), payload)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"windows_install_profile": profile})
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"join_bundle": bundle})
|
||||
}
|
||||
|
||||
func (m *Module) dockerInstallProfile(w http.ResponseWriter, r *http.Request) {
|
||||
func (m *Module) dockerJoinBundle(w http.ResponseWriter, r *http.Request) {
|
||||
var payload clustermodule.DockerInstallProfileRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
httpx.WriteError(w, http.StatusBadRequest, "invalid docker install profile payload")
|
||||
httpx.WriteError(w, http.StatusBadRequest, "invalid docker join bundle payload")
|
||||
return
|
||||
}
|
||||
profile, err := m.cluster.GetDockerInstallProfile(r.Context(), payload)
|
||||
bundle, err := m.cluster.GetDockerJoinBundle(r.Context(), payload)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"docker_install_profile": profile})
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"join_bundle": bundle})
|
||||
}
|
||||
|
||||
func (m *Module) registerFabricNode(w http.ResponseWriter, r *http.Request) {
|
||||
var payload struct {
|
||||
ClusterID string `json:"cluster_id"`
|
||||
NodeKey string `json:"node_key"`
|
||||
Name string `json:"name"`
|
||||
OwnershipType string `json:"ownership_type"`
|
||||
OwnerOrganizationID *string `json:"owner_organization_id"`
|
||||
ReportedVersion *string `json:"reported_version"`
|
||||
Metadata json.RawMessage `json:"metadata"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
httpx.WriteError(w, http.StatusBadRequest, "invalid fabric node registration payload")
|
||||
return
|
||||
}
|
||||
item, err := m.cluster.RegisterFabricNode(r.Context(), clustermodule.RegisterFabricNodeInput{
|
||||
ClusterID: payload.ClusterID,
|
||||
NodeKey: payload.NodeKey,
|
||||
Name: payload.Name,
|
||||
OwnershipType: payload.OwnershipType,
|
||||
OwnerOrganizationID: payload.OwnerOrganizationID,
|
||||
ReportedVersion: payload.ReportedVersion,
|
||||
Metadata: payload.Metadata,
|
||||
})
|
||||
if err != nil {
|
||||
httpx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{
|
||||
"status": "registered",
|
||||
"node_id": item.ID,
|
||||
"node": item,
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Module) enrollAgent(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -133,17 +154,17 @@ func (m *Module) enrollAgent(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Module) bootstrapEnrollment(w http.ResponseWriter, r *http.Request) {
|
||||
func (m *Module) fetchEnrollmentJoinContract(w http.ResponseWriter, r *http.Request) {
|
||||
var payload struct {
|
||||
ClusterID string `json:"cluster_id"`
|
||||
NodeFingerprint string `json:"node_fingerprint"`
|
||||
PublicKey string `json:"public_key"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
httpx.WriteError(w, http.StatusBadRequest, "invalid enrollment bootstrap payload")
|
||||
httpx.WriteError(w, http.StatusBadRequest, "invalid enrollment join payload")
|
||||
return
|
||||
}
|
||||
result, err := m.cluster.GetJoinRequestBootstrap(r.Context(), clustermodule.GetJoinRequestBootstrapInput{
|
||||
result, err := m.cluster.GetJoinRequestJoin(r.Context(), clustermodule.GetJoinRequestJoinInput{
|
||||
ClusterID: payload.ClusterID,
|
||||
JoinRequestID: chi.URLParam(r, "requestID"),
|
||||
NodeFingerprint: payload.NodeFingerprint,
|
||||
@@ -155,261 +176,3 @@ func (m *Module) bootstrapEnrollment(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, result)
|
||||
}
|
||||
|
||||
func (m *Module) registerAgent(w http.ResponseWriter, r *http.Request) {
|
||||
var payload struct {
|
||||
ClusterID string `json:"cluster_id"`
|
||||
NodeKey string `json:"node_key"`
|
||||
Name string `json:"name"`
|
||||
OwnershipType string `json:"ownership_type"`
|
||||
OwnerOrganizationID *string `json:"owner_organization_id"`
|
||||
ReportedVersion *string `json:"reported_version"`
|
||||
Metadata json.RawMessage `json:"metadata"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
httpx.WriteError(w, http.StatusBadRequest, "invalid agent registration payload")
|
||||
return
|
||||
}
|
||||
if payload.NodeKey == "" || payload.Name == "" || payload.OwnershipType == "" {
|
||||
httpx.WriteError(w, http.StatusBadRequest, "node_key, name, and ownership_type are required")
|
||||
return
|
||||
}
|
||||
if len(payload.Metadata) == 0 {
|
||||
payload.Metadata = json.RawMessage(`{}`)
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
nodeID := uuid.NewString()
|
||||
if err := m.db.QueryRow(r.Context(), `
|
||||
INSERT INTO nodes (
|
||||
id, owner_organization_id, node_key, name, ownership_type, registration_status, health_status,
|
||||
version_state, partition_state, desired_version, reported_version, last_seen_at, metadata, created_at, updated_at
|
||||
) VALUES ($1, $2, $3, $4, $5, 'active', 'unknown', 'unknown', 'healthy', NULL, $6, $7, $8::jsonb, $9, $10)
|
||||
ON CONFLICT (node_key) DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
ownership_type = EXCLUDED.ownership_type,
|
||||
owner_organization_id = EXCLUDED.owner_organization_id,
|
||||
registration_status = 'active',
|
||||
reported_version = EXCLUDED.reported_version,
|
||||
last_seen_at = EXCLUDED.last_seen_at,
|
||||
metadata = EXCLUDED.metadata,
|
||||
updated_at = EXCLUDED.updated_at
|
||||
RETURNING id
|
||||
`, nodeID, payload.OwnerOrganizationID, payload.NodeKey, payload.Name, payload.OwnershipType, payload.ReportedVersion, now, []byte(payload.Metadata), now, now).Scan(&nodeID); err != nil {
|
||||
httpx.WriteError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if payload.ClusterID != "" {
|
||||
if _, err := m.db.Exec(r.Context(), `
|
||||
INSERT INTO cluster_memberships (cluster_id, node_id, membership_status, joined_at, last_seen_at, metadata)
|
||||
VALUES ($1::uuid, $2::uuid, 'active', $3, $3, $4::jsonb)
|
||||
ON CONFLICT (cluster_id, node_id) DO UPDATE SET
|
||||
membership_status = 'active',
|
||||
last_seen_at = EXCLUDED.last_seen_at,
|
||||
metadata = cluster_memberships.metadata || EXCLUDED.metadata
|
||||
`, payload.ClusterID, nodeID, now, []byte(`{"source":"fabric_control_candidate_registration"}`)); err != nil {
|
||||
httpx.WriteError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{
|
||||
"node_id": nodeID,
|
||||
"status": "registered",
|
||||
"legacy": true,
|
||||
"warning": "direct node-agent registration is retained for compatibility; production enrollment must use /node-agents/enroll",
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Module) reportHealth(w http.ResponseWriter, r *http.Request) {
|
||||
var payload struct {
|
||||
HealthStatus string `json:"health_status"`
|
||||
ReportedVersion *string `json:"reported_version"`
|
||||
Metadata json.RawMessage `json:"metadata"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
httpx.WriteError(w, http.StatusBadRequest, "invalid node health payload")
|
||||
return
|
||||
}
|
||||
if payload.HealthStatus == "" {
|
||||
payload.HealthStatus = "unknown"
|
||||
}
|
||||
if len(payload.Metadata) == 0 {
|
||||
payload.Metadata = json.RawMessage(`{}`)
|
||||
}
|
||||
if _, err := m.db.Exec(r.Context(), `
|
||||
UPDATE nodes
|
||||
SET health_status = $2, reported_version = COALESCE($3, reported_version), last_seen_at = $4, metadata = $5::jsonb, updated_at = $4
|
||||
WHERE id = $1
|
||||
`, chi.URLParam(r, "nodeID"), payload.HealthStatus, payload.ReportedVersion, time.Now().UTC(), []byte(payload.Metadata)); err != nil {
|
||||
httpx.WriteError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusAccepted, map[string]any{"status": "accepted"})
|
||||
}
|
||||
|
||||
func (m *Module) reportServiceStatus(w http.ResponseWriter, r *http.Request) {
|
||||
var payload struct {
|
||||
Services []struct {
|
||||
ServiceType string `json:"service_type"`
|
||||
ReportedState string `json:"reported_state"`
|
||||
Metadata json.RawMessage `json:"metadata"`
|
||||
} `json:"services"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
httpx.WriteError(w, http.StatusBadRequest, "invalid node service status payload")
|
||||
return
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
for _, service := range payload.Services {
|
||||
if len(service.Metadata) == 0 {
|
||||
service.Metadata = json.RawMessage(`{}`)
|
||||
}
|
||||
if _, err := m.db.Exec(r.Context(), `
|
||||
INSERT INTO node_services (
|
||||
node_id, service_type, enabled, desired_state, reported_state, last_reported_at, metadata, updated_at
|
||||
) VALUES ($1, $2, FALSE, 'disabled', $3, $4, $5::jsonb, $4)
|
||||
ON CONFLICT (node_id, service_type) DO UPDATE SET
|
||||
reported_state = EXCLUDED.reported_state,
|
||||
last_reported_at = EXCLUDED.last_reported_at,
|
||||
metadata = EXCLUDED.metadata,
|
||||
updated_at = EXCLUDED.updated_at
|
||||
`, chi.URLParam(r, "nodeID"), service.ServiceType, service.ReportedState, now, []byte(service.Metadata)); err != nil {
|
||||
httpx.WriteError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusAccepted, map[string]any{"status": "accepted"})
|
||||
}
|
||||
|
||||
func (m *Module) listVPNAssignments(w http.ResponseWriter, r *http.Request) {
|
||||
items, err := m.cluster.ListNodeVPNAssignments(r.Context(), chi.URLParam(r, "clusterID"), chi.URLParam(r, "nodeID"))
|
||||
if writeClusterServiceError(w, err) {
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{
|
||||
"vpn_assignments": items,
|
||||
"runtime_execution_enabled": false,
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Module) reportVPNAssignmentStatus(w http.ResponseWriter, r *http.Request) {
|
||||
var payload struct {
|
||||
ObservedStatus string `json:"observed_status"`
|
||||
StatusPayload json.RawMessage `json:"status_payload"`
|
||||
ObservedAt *time.Time `json:"observed_at"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
httpx.WriteError(w, http.StatusBadRequest, "invalid vpn assignment status payload")
|
||||
return
|
||||
}
|
||||
observedAt := time.Time{}
|
||||
if payload.ObservedAt != nil {
|
||||
observedAt = *payload.ObservedAt
|
||||
}
|
||||
item, err := m.cluster.ReportNodeVPNAssignmentStatus(r.Context(), clustermodule.ReportNodeVPNAssignmentStatusInput{
|
||||
ClusterID: chi.URLParam(r, "clusterID"),
|
||||
NodeID: chi.URLParam(r, "nodeID"),
|
||||
VPNConnectionID: chi.URLParam(r, "vpnConnectionID"),
|
||||
ObservedStatus: payload.ObservedStatus,
|
||||
StatusPayload: payload.StatusPayload,
|
||||
ObservedAt: observedAt,
|
||||
})
|
||||
if writeClusterServiceError(w, err) {
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusAccepted, map[string]any{
|
||||
"vpn_assignment_status": item,
|
||||
"runtime_execution_enabled": false,
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Module) requestUpdateManifest(w http.ResponseWriter, r *http.Request) {
|
||||
nodeID := chi.URLParam(r, "nodeID")
|
||||
var mode, channel string
|
||||
var canary, automatic bool
|
||||
var desiredVersion *string
|
||||
if err := m.db.QueryRow(r.Context(), `
|
||||
SELECT n.desired_version, p.mode, p.channel, p.canary, p.automatic_rollout
|
||||
FROM nodes n
|
||||
LEFT JOIN node_update_policies p ON p.node_id = n.id
|
||||
WHERE n.id = $1
|
||||
`, nodeID).Scan(&desiredVersion, &mode, &channel, &canary, &automatic); err != nil {
|
||||
httpx.WriteError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{
|
||||
"manifest": map[string]any{
|
||||
"node_id": nodeID,
|
||||
"desired_version": desiredVersion,
|
||||
"mode": mode,
|
||||
"channel": channel,
|
||||
"canary": canary,
|
||||
"automatic_rollout": automatic,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Module) acknowledgeUpdateResult(w http.ResponseWriter, r *http.Request) {
|
||||
m.recordUpdateRun(w, r, "update")
|
||||
}
|
||||
|
||||
func (m *Module) reportRollbackResult(w http.ResponseWriter, r *http.Request) {
|
||||
m.recordUpdateRun(w, r, "rollback")
|
||||
}
|
||||
|
||||
func (m *Module) recordUpdateRun(w http.ResponseWriter, r *http.Request, action string) {
|
||||
var payload struct {
|
||||
TargetVersion string `json:"target_version"`
|
||||
Status string `json:"status"`
|
||||
Payload json.RawMessage `json:"payload"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
httpx.WriteError(w, http.StatusBadRequest, "invalid update result payload")
|
||||
return
|
||||
}
|
||||
if payload.Status == "" {
|
||||
payload.Status = "acknowledged"
|
||||
}
|
||||
if len(payload.Payload) == 0 {
|
||||
payload.Payload = json.RawMessage(`{}`)
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
runID := uuid.NewString()
|
||||
if _, err := m.db.Exec(r.Context(), `
|
||||
INSERT INTO node_agent_update_runs (
|
||||
id, node_id, action, target_version, status, requested_at, acknowledged_at, completed_at, payload
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $6, CASE WHEN $5 IN ('succeeded', 'failed') THEN $6 ELSE NULL END, $7::jsonb)
|
||||
`, runID, chi.URLParam(r, "nodeID"), action, payload.TargetVersion, payload.Status, now, []byte(payload.Payload)); err != nil {
|
||||
httpx.WriteError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if action == "update" && payload.Status == "succeeded" {
|
||||
_, _ = m.db.Exec(r.Context(), `
|
||||
UPDATE nodes
|
||||
SET reported_version = $2, version_state = 'current', updated_at = $3
|
||||
WHERE id = $1
|
||||
`, chi.URLParam(r, "nodeID"), payload.TargetVersion, now)
|
||||
}
|
||||
if action == "rollback" && payload.Status == "succeeded" {
|
||||
_, _ = m.db.Exec(r.Context(), `
|
||||
UPDATE nodes
|
||||
SET reported_version = $2, version_state = 'rollback', updated_at = $3
|
||||
WHERE id = $1
|
||||
`, chi.URLParam(r, "nodeID"), payload.TargetVersion, now)
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusAccepted, map[string]any{"status": "accepted", "run_id": runID})
|
||||
}
|
||||
|
||||
func writeClusterServiceError(w http.ResponseWriter, err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
switch {
|
||||
case errors.Is(err, clustermodule.ErrVPNLeaseOwnerNotAllowed), errors.Is(err, clustermodule.ErrVPNLeaseOwnerRoleRequired):
|
||||
httpx.WriteError(w, http.StatusForbidden, err.Error())
|
||||
case errors.Is(err, clustermodule.ErrInvalidPayload):
|
||||
httpx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
default:
|
||||
httpx.WriteError(w, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user