package nodeagent import ( "encoding/json" "net/http" "github.com/go-chi/chi/v5" clustermodule "github.com/example/remote-access-platform/backend/internal/modules/cluster" "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 { cluster *clustermodule.Service } func NewModule(deps module.Dependencies) *Module { clusterStore := clustermodule.NewPostgresStore(deps.Infra.DB) if deps.Config.Secret.EncryptionKeyBase64 != "" { if encryptor, err := secrets.NewEncryptor(deps.Config.Secret.EncryptionKeyBase64, deps.Config.Secret.EncryptionKeyID); err == nil { clusterStore.WithClusterKeyEncryptor(encryptor) } } return &Module{ cluster: clustermodule.NewService(clusterStore), } } func (m *Module) Name() string { return "nodeagent" } func (m *Module) RegisterRoutes(router chi.Router) { router.Route("/node-agents", func(r chi.Router) { 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}/join", m.fetchEnrollmentJoinContract) }) } 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 join bundle payload") return } 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{"join_bundle": bundle}) } 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 join bundle payload") return } 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{"join_bundle": bundle}) } 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 join bundle payload") return } 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{"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) { var payload struct { ClusterID string `json:"cluster_id"` 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 agent enrollment payload") return } joinRequest, err := m.cluster.CreateJoinRequest(r.Context(), clustermodule.CreateJoinRequestInput{ ClusterID: payload.ClusterID, JoinToken: payload.JoinToken, NodeName: payload.NodeName, NodeFingerprint: payload.NodeFingerprint, PublicKey: payload.PublicKey, ReportedCapabilities: payload.ReportedCapabilities, ReportedFacts: payload.ReportedFacts, RequestedRoles: payload.RequestedRoles, }) if err != nil { httpx.WriteError(w, http.StatusBadRequest, err.Error()) return } httpx.WriteJSON(w, http.StatusAccepted, map[string]any{ "status": "pending_approval", "join_request": joinRequest, }) } 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 join payload") return } result, err := m.cluster.GetJoinRequestJoin(r.Context(), clustermodule.GetJoinRequestJoinInput{ ClusterID: payload.ClusterID, JoinRequestID: chi.URLParam(r, "requestID"), NodeFingerprint: payload.NodeFingerprint, PublicKey: payload.PublicKey, }) if err != nil { httpx.WriteError(w, http.StatusBadRequest, err.Error()) return } httpx.WriteJSON(w, http.StatusOK, result) }