Initial project snapshot

This commit is contained in:
2026-04-28 22:29:50 +03:00
commit 8ba0561f4f
365 changed files with 91832 additions and 0 deletions
+458
View File
@@ -0,0 +1,458 @@
package node
import (
"context"
"encoding/json"
"errors"
"net/http"
"time"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/example/remote-access-platform/backend/internal/platform/httpx"
"github.com/example/remote-access-platform/backend/internal/platform/module"
)
type Module struct {
db *pgxpool.Pool
}
type Node struct {
ID string `json:"id"`
OwnerOrganizationID *string `json:"owner_organization_id,omitempty"`
NodeKey string `json:"node_key"`
Name string `json:"name"`
OwnershipType string `json:"ownership_type"`
RegistrationStatus string `json:"registration_status"`
HealthStatus string `json:"health_status"`
VersionState string `json:"version_state"`
PartitionState string `json:"partition_state"`
DesiredVersion *string `json:"desired_version,omitempty"`
ReportedVersion *string `json:"reported_version,omitempty"`
LastSeenAt *time.Time `json:"last_seen_at,omitempty"`
Metadata json.RawMessage `json:"metadata"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type NodeCapability struct {
NodeID string `json:"node_id"`
Capability string `json:"capability"`
Value json.RawMessage `json:"value"`
UpdatedAt time.Time `json:"updated_at"`
}
type NodeService struct {
NodeID string `json:"node_id"`
ServiceType string `json:"service_type"`
Enabled bool `json:"enabled"`
DesiredState string `json:"desired_state"`
ReportedState string `json:"reported_state"`
LastReportedAt *time.Time `json:"last_reported_at,omitempty"`
Metadata json.RawMessage `json:"metadata"`
UpdatedAt time.Time `json:"updated_at"`
}
type NodeUpdatePolicy struct {
NodeID string `json:"node_id"`
Mode string `json:"mode"`
Channel string `json:"channel"`
MaintenanceWindow json.RawMessage `json:"maintenance_window"`
Canary bool `json:"canary"`
AutomaticRollout bool `json:"automatic_rollout"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type NodePartitionState struct {
NodeID string `json:"node_id"`
ClusterState string `json:"cluster_state"`
RecoveryMode string `json:"recovery_mode"`
Notes *string `json:"notes,omitempty"`
UpdatedAt time.Time `json:"updated_at"`
}
type upsertNodeRequest struct {
ActorUserID string `json:"actor_user_id"`
OwnerOrganizationID *string `json:"owner_organization_id"`
NodeKey string `json:"node_key"`
Name string `json:"name"`
OwnershipType string `json:"ownership_type"`
DesiredVersion *string `json:"desired_version"`
Metadata json.RawMessage `json:"metadata"`
Capabilities []struct {
Capability string `json:"capability"`
Value json.RawMessage `json:"value"`
} `json:"capabilities"`
Services []struct {
ServiceType string `json:"service_type"`
Enabled bool `json:"enabled"`
DesiredState string `json:"desired_state"`
Metadata json.RawMessage `json:"metadata"`
} `json:"services"`
UpdatePolicy struct {
Mode string `json:"mode"`
Channel string `json:"channel"`
MaintenanceWindow json.RawMessage `json:"maintenance_window"`
Canary bool `json:"canary"`
AutomaticRollout bool `json:"automatic_rollout"`
} `json:"update_policy"`
PartitionState struct {
ClusterState string `json:"cluster_state"`
RecoveryMode string `json:"recovery_mode"`
Notes *string `json:"notes"`
} `json:"partition_state"`
}
func NewModule(deps module.Dependencies) *Module {
return &Module{db: deps.Infra.DB}
}
func (m *Module) Name() string {
return "node"
}
func (m *Module) RegisterRoutes(router chi.Router) {
router.Route("/nodes", func(r chi.Router) {
r.Get("/", m.listNodes)
r.Post("/", m.createNode)
r.Get("/{nodeID}", m.getNode)
r.Put("/{nodeID}", m.updateNode)
})
}
func (m *Module) listNodes(w http.ResponseWriter, r *http.Request) {
rows, err := m.db.Query(r.Context(), `
SELECT 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
FROM nodes
ORDER BY created_at DESC
`)
if err != nil {
httpx.WriteError(w, http.StatusInternalServerError, err.Error())
return
}
defer rows.Close()
var items []Node
for rows.Next() {
item, err := scanNode(rows)
if err != nil {
httpx.WriteError(w, http.StatusInternalServerError, err.Error())
return
}
items = append(items, item)
}
httpx.WriteJSON(w, http.StatusOK, map[string]any{"nodes": items})
}
func (m *Module) getNode(w http.ResponseWriter, r *http.Request) {
nodeID := chi.URLParam(r, "nodeID")
item, err := m.getNodeByID(r.Context(), nodeID)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
httpx.WriteError(w, http.StatusNotFound, "node not found")
return
}
httpx.WriteError(w, http.StatusInternalServerError, err.Error())
return
}
caps, _ := m.listCapabilities(r.Context(), nodeID)
services, _ := m.listServices(r.Context(), nodeID)
updatePolicy, _ := m.getUpdatePolicy(r.Context(), nodeID)
partitionState, _ := m.getPartitionState(r.Context(), nodeID)
httpx.WriteJSON(w, http.StatusOK, map[string]any{
"node": item,
"capabilities": caps,
"services": services,
"update_policy": updatePolicy,
"partition_state": partitionState,
})
}
func (m *Module) createNode(w http.ResponseWriter, r *http.Request) {
req, err := decodeNodeRequest(r)
if err != nil {
httpx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
item := Node{
ID: uuid.NewString(),
OwnerOrganizationID: req.OwnerOrganizationID,
NodeKey: req.NodeKey,
Name: req.Name,
OwnershipType: req.OwnershipType,
RegistrationStatus: "pending",
HealthStatus: "unknown",
VersionState: "unknown",
PartitionState: "healthy",
DesiredVersion: req.DesiredVersion,
Metadata: req.Metadata,
CreatedAt: time.Now().UTC(),
UpdatedAt: time.Now().UTC(),
}
if err := m.persistNode(r.Context(), item, req, true); err != nil {
httpx.WriteError(w, http.StatusInternalServerError, err.Error())
return
}
httpx.WriteJSON(w, http.StatusCreated, map[string]any{"node": item})
}
func (m *Module) updateNode(w http.ResponseWriter, r *http.Request) {
req, err := decodeNodeRequest(r)
if err != nil {
httpx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
nodeID := chi.URLParam(r, "nodeID")
item, err := m.getNodeByID(r.Context(), nodeID)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
httpx.WriteError(w, http.StatusNotFound, "node not found")
return
}
httpx.WriteError(w, http.StatusInternalServerError, err.Error())
return
}
item.OwnerOrganizationID = req.OwnerOrganizationID
item.NodeKey = req.NodeKey
item.Name = req.Name
item.OwnershipType = req.OwnershipType
item.DesiredVersion = req.DesiredVersion
item.Metadata = req.Metadata
item.UpdatedAt = time.Now().UTC()
if err := m.persistNode(r.Context(), item, req, false); err != nil {
httpx.WriteError(w, http.StatusInternalServerError, err.Error())
return
}
httpx.WriteJSON(w, http.StatusOK, map[string]any{"node": item})
}
func decodeNodeRequest(r *http.Request) (*upsertNodeRequest, error) {
var req upsertNodeRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, errors.New("invalid node payload")
}
if req.ActorUserID == "" || req.NodeKey == "" || req.Name == "" || req.OwnershipType == "" {
return nil, errors.New("actor_user_id, node_key, name, and ownership_type are required")
}
if len(req.Metadata) == 0 {
req.Metadata = json.RawMessage(`{}`)
}
if !json.Valid(req.Metadata) {
return nil, errors.New("metadata must be valid json")
}
if req.UpdatePolicy.Mode == "" {
req.UpdatePolicy.Mode = "manual"
}
if req.UpdatePolicy.Channel == "" {
req.UpdatePolicy.Channel = "stable"
}
if len(req.UpdatePolicy.MaintenanceWindow) == 0 {
req.UpdatePolicy.MaintenanceWindow = json.RawMessage(`{}`)
}
if req.PartitionState.ClusterState == "" {
req.PartitionState.ClusterState = "healthy"
}
if req.PartitionState.RecoveryMode == "" {
req.PartitionState.RecoveryMode = "normal"
}
for i := range req.Capabilities {
if len(req.Capabilities[i].Value) == 0 {
req.Capabilities[i].Value = json.RawMessage(`{}`)
}
}
for i := range req.Services {
if req.Services[i].DesiredState == "" {
req.Services[i].DesiredState = "disabled"
}
if len(req.Services[i].Metadata) == 0 {
req.Services[i].Metadata = json.RawMessage(`{}`)
}
}
return &req, nil
}
func (m *Module) persistNode(ctx context.Context, item Node, req *upsertNodeRequest, create bool) error {
tx, err := m.db.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
if create {
_, err = tx.Exec(ctx, `
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,$6,$7,$8,$9,$10,$11,$12,$13::jsonb,$14,$15)
`, item.ID, item.OwnerOrganizationID, item.NodeKey, item.Name, item.OwnershipType, item.RegistrationStatus, item.HealthStatus, item.VersionState, item.PartitionState, item.DesiredVersion, item.ReportedVersion, item.LastSeenAt, []byte(item.Metadata), item.CreatedAt, item.UpdatedAt)
} else {
_, err = tx.Exec(ctx, `
UPDATE nodes
SET owner_organization_id=$2, node_key=$3, name=$4, ownership_type=$5, desired_version=$6, metadata=$7::jsonb, updated_at=$8
WHERE id=$1
`, item.ID, item.OwnerOrganizationID, item.NodeKey, item.Name, item.OwnershipType, item.DesiredVersion, []byte(item.Metadata), item.UpdatedAt)
}
if err != nil {
return err
}
if _, err := tx.Exec(ctx, `DELETE FROM node_capabilities WHERE node_id = $1`, item.ID); err != nil {
return err
}
for _, capability := range req.Capabilities {
if _, err := tx.Exec(ctx, `
INSERT INTO node_capabilities (node_id, capability, value, updated_at)
VALUES ($1, $2, $3::jsonb, $4)
`, item.ID, capability.Capability, []byte(capability.Value), time.Now().UTC()); err != nil {
return err
}
}
if _, err := tx.Exec(ctx, `DELETE FROM node_services WHERE node_id = $1`, item.ID); err != nil {
return err
}
for _, service := range req.Services {
if _, err := tx.Exec(ctx, `
INSERT INTO node_services (
node_id, service_type, enabled, desired_state, reported_state, last_reported_at, metadata, updated_at
) VALUES ($1, $2, $3, $4, 'unknown', NULL, $5::jsonb, $6)
`, item.ID, service.ServiceType, service.Enabled, service.DesiredState, []byte(service.Metadata), time.Now().UTC()); err != nil {
return err
}
}
if _, err := tx.Exec(ctx, `
INSERT INTO node_update_policies (
node_id, mode, channel, maintenance_window, canary, automatic_rollout, created_at, updated_at
) VALUES ($1,$2,$3,$4::jsonb,$5,$6,$7,$8)
ON CONFLICT (node_id) DO UPDATE SET
mode = EXCLUDED.mode,
channel = EXCLUDED.channel,
maintenance_window = EXCLUDED.maintenance_window,
canary = EXCLUDED.canary,
automatic_rollout = EXCLUDED.automatic_rollout,
updated_at = EXCLUDED.updated_at
`, item.ID, req.UpdatePolicy.Mode, req.UpdatePolicy.Channel, []byte(req.UpdatePolicy.MaintenanceWindow), req.UpdatePolicy.Canary, req.UpdatePolicy.AutomaticRollout, time.Now().UTC(), time.Now().UTC()); err != nil {
return err
}
if _, err := tx.Exec(ctx, `
INSERT INTO node_partition_states (node_id, cluster_state, recovery_mode, notes, updated_at)
VALUES ($1,$2,$3,$4,$5)
ON CONFLICT (node_id) DO UPDATE SET
cluster_state = EXCLUDED.cluster_state,
recovery_mode = EXCLUDED.recovery_mode,
notes = EXCLUDED.notes,
updated_at = EXCLUDED.updated_at
`, item.ID, req.PartitionState.ClusterState, req.PartitionState.RecoveryMode, req.PartitionState.Notes, time.Now().UTC()); err != nil {
return err
}
return tx.Commit(ctx)
}
func (m *Module) getNodeByID(ctx context.Context, nodeID string) (Node, error) {
row := m.db.QueryRow(ctx, `
SELECT 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
FROM nodes
WHERE id = $1
`, nodeID)
return scanNode(row)
}
func (m *Module) listCapabilities(ctx context.Context, nodeID string) ([]NodeCapability, error) {
rows, err := m.db.Query(ctx, `SELECT node_id, capability, value, updated_at FROM node_capabilities WHERE node_id = $1 ORDER BY capability`, nodeID)
if err != nil {
return nil, err
}
defer rows.Close()
var out []NodeCapability
for rows.Next() {
var item NodeCapability
if err := rows.Scan(&item.NodeID, &item.Capability, &item.Value, &item.UpdatedAt); err != nil {
return nil, err
}
out = append(out, item)
}
return out, rows.Err()
}
func (m *Module) listServices(ctx context.Context, nodeID string) ([]NodeService, error) {
rows, err := m.db.Query(ctx, `
SELECT node_id, service_type, enabled, desired_state, reported_state, last_reported_at, metadata, updated_at
FROM node_services WHERE node_id = $1 ORDER BY service_type
`, nodeID)
if err != nil {
return nil, err
}
defer rows.Close()
var out []NodeService
for rows.Next() {
var item NodeService
if err := rows.Scan(&item.NodeID, &item.ServiceType, &item.Enabled, &item.DesiredState, &item.ReportedState, &item.LastReportedAt, &item.Metadata, &item.UpdatedAt); err != nil {
return nil, err
}
out = append(out, item)
}
return out, rows.Err()
}
func (m *Module) getUpdatePolicy(ctx context.Context, nodeID string) (*NodeUpdatePolicy, error) {
row := m.db.QueryRow(ctx, `
SELECT node_id, mode, channel, maintenance_window, canary, automatic_rollout, created_at, updated_at
FROM node_update_policies WHERE node_id = $1
`, nodeID)
var item NodeUpdatePolicy
if err := row.Scan(&item.NodeID, &item.Mode, &item.Channel, &item.MaintenanceWindow, &item.Canary, &item.AutomaticRollout, &item.CreatedAt, &item.UpdatedAt); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, nil
}
return nil, err
}
return &item, nil
}
func (m *Module) getPartitionState(ctx context.Context, nodeID string) (*NodePartitionState, error) {
row := m.db.QueryRow(ctx, `
SELECT node_id, cluster_state, recovery_mode, notes, updated_at
FROM node_partition_states WHERE node_id = $1
`, nodeID)
var item NodePartitionState
if err := row.Scan(&item.NodeID, &item.ClusterState, &item.RecoveryMode, &item.Notes, &item.UpdatedAt); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, nil
}
return nil, err
}
return &item, nil
}
type rowScanner interface {
Scan(dest ...any) error
}
func scanNode(row rowScanner) (Node, error) {
var item Node
if err := row.Scan(
&item.ID,
&item.OwnerOrganizationID,
&item.NodeKey,
&item.Name,
&item.OwnershipType,
&item.RegistrationStatus,
&item.HealthStatus,
&item.VersionState,
&item.PartitionState,
&item.DesiredVersion,
&item.ReportedVersion,
&item.LastSeenAt,
&item.Metadata,
&item.CreatedAt,
&item.UpdatedAt,
); err != nil {
return Node{}, err
}
if len(item.Metadata) == 0 {
item.Metadata = json.RawMessage(`{}`)
}
return item, nil
}