Files
HexaHost-GameCloud/apps/node-agent/internal/protocol/messages.go

386 lines
12 KiB
Go

package protocol
import (
"encoding/json"
"fmt"
"strings"
"time"
)
const CurrentProtocolVersion = 1
// MessageType identifies a versioned agent/control-plane message.
type MessageType string
const (
// Agent → Control Plane
TypeAgentHello MessageType = "agent.hello"
TypeAgentHeartbeat MessageType = "agent.heartbeat"
TypeAgentInventory MessageType = "agent.inventory"
TypeServerState MessageType = "server.state"
TypeServerMetrics MessageType = "server.metrics"
TypeServerLog MessageType = "server.log"
TypeServerOperationProgress MessageType = "server.operation.progress"
TypeServerOperationComplete MessageType = "server.operation.completed"
TypeServerOperationFailed MessageType = "server.operation.failed"
// Control Plane → Agent
TypeServerProvision MessageType = "server.provision"
TypeServerInstall MessageType = "server.install"
TypeServerStart MessageType = "server.start"
TypeServerStop MessageType = "server.stop"
TypeServerKill MessageType = "server.kill"
TypeServerDelete MessageType = "server.delete"
TypeServerInspect MessageType = "server.inspect"
TypeServerCommand MessageType = "server.command"
TypeServerBackupPrepare MessageType = "server.backup.prepare"
TypeServerBackupRelease MessageType = "server.backup.release"
TypeServerFilesList MessageType = "server.files.list"
TypeServerFilesRead MessageType = "server.files.read"
TypeServerFilesWrite MessageType = "server.files.write"
TypeServerFilesDelete MessageType = "server.files.delete"
TypeServerFilesArchive MessageType = "server.files.archive"
TypeServerFilesUnarchive MessageType = "server.files.unarchive"
TypeServerFilesUploadDone MessageType = "server.files.upload.complete"
TypeServerWorldValidate MessageType = "server.world.validate"
TypeServerWorldArchive MessageType = "server.world.archive"
TypeServerWorldReplace MessageType = "server.world.replace"
TypeServerStorageUpload MessageType = "server.storage.upload"
TypeServerStorageDownload MessageType = "server.storage.download"
TypeServerAddonInstall MessageType = "server.addon.install"
TypeServerAddonRemove MessageType = "server.addon.remove"
TypeServerMetricsSub MessageType = "server.metrics.subscribe"
TypeServerLogsSubscribe MessageType = "server.logs.subscribe"
)
var agentToControlTypes = map[MessageType]struct{}{
TypeAgentHello: {},
TypeAgentHeartbeat: {},
TypeAgentInventory: {},
TypeServerState: {},
TypeServerMetrics: {},
TypeServerLog: {},
TypeServerOperationProgress: {},
TypeServerOperationComplete: {},
TypeServerOperationFailed: {},
}
var controlToAgentTypes = map[MessageType]struct{}{
TypeServerProvision: {},
TypeServerInstall: {},
TypeServerStart: {},
TypeServerStop: {},
TypeServerKill: {},
TypeServerDelete: {},
TypeServerInspect: {},
TypeServerCommand: {},
TypeServerBackupPrepare: {},
TypeServerBackupRelease: {},
TypeServerFilesList: {},
TypeServerFilesRead: {},
TypeServerFilesWrite: {},
TypeServerFilesDelete: {},
TypeServerFilesArchive: {},
TypeServerFilesUnarchive: {},
TypeServerFilesUploadDone: {},
TypeServerWorldValidate: {},
TypeServerWorldArchive: {},
TypeServerWorldReplace: {},
TypeServerStorageUpload: {},
TypeServerStorageDownload: {},
TypeServerAddonInstall: {},
TypeServerAddonRemove: {},
TypeServerMetricsSub: {},
TypeServerLogsSubscribe: {},
}
// Envelope is the common wrapper for all protocol messages.
type Envelope struct {
ProtocolVersion int `json:"protocolVersion"`
MessageID string `json:"messageId"`
CorrelationID string `json:"correlationId,omitempty"`
Type MessageType `json:"type"`
Timestamp time.Time `json:"timestamp"`
Payload json.RawMessage `json:"payload"`
}
// OperationMeta is shared metadata for lifecycle operations.
type OperationMeta struct {
ServerID string `json:"serverId"`
Generation int64 `json:"generation"`
Deadline *time.Time `json:"deadline,omitempty"`
IdempotencyKey string `json:"idempotencyKey,omitempty"`
}
// AgentHelloPayload is sent when the agent connects to the control plane.
type AgentHelloPayload struct {
NodeID string `json:"nodeId"`
AgentVersion string `json:"agentVersion"`
ProtocolVersion int `json:"protocolVersion"`
Hostname string `json:"hostname,omitempty"`
Capabilities []string `json:"capabilities,omitempty"`
}
// AgentHeartbeatPayload reports node health and resource usage.
type AgentHeartbeatPayload struct {
NodeID string `json:"nodeId"`
CPUUsagePercent float64 `json:"cpuUsagePercent"`
MemoryUsedBytes int64 `json:"memoryUsedBytes"`
MemoryTotalBytes int64 `json:"memoryTotalBytes"`
DiskUsedBytes int64 `json:"diskUsedBytes"`
DiskTotalBytes int64 `json:"diskTotalBytes"`
RunningServers int `json:"runningServers"`
}
// ServerStartPayload instructs the agent to start a game server.
type ServerStartPayload struct {
OperationMeta
DesiredGeneration int64 `json:"desiredGeneration"`
}
// ServerProvisionPayload instructs the agent to create a Minecraft server container.
type ServerProvisionPayload struct {
OperationMeta
Edition string `json:"edition"`
SoftwareFamily string `json:"softwareFamily"`
MinecraftVersion string `json:"minecraftVersion"`
HostPort int `json:"hostPort"`
RamMb int `json:"ramMb"`
DataPath string `json:"dataPath"`
EulaAccepted bool `json:"eulaAccepted"`
}
// ServerStopPayload instructs the agent to stop a running game server.
type ServerStopPayload struct {
OperationMeta
}
// ServerCommandPayload instructs the agent to run a console command via RCON.
type ServerCommandPayload struct {
OperationMeta
Command string `json:"command"`
}
// ServerLogsSubscribePayload instructs the agent to stream container logs.
type ServerLogsSubscribePayload struct {
OperationMeta
TailLines int `json:"tailLines"`
Follow bool `json:"follow"`
}
// ServerLogPayload carries a single log line from a game server.
type ServerLogPayload struct {
OperationMeta
Line string `json:"line"`
Stream string `json:"stream"`
}
// ServerFilesListPayload lists files in a server data directory.
type ServerFilesListPayload struct {
OperationMeta
Path string `json:"path"`
}
// ServerFilesReadPayload reads a file from the server data directory.
type ServerFilesReadPayload struct {
OperationMeta
Path string `json:"path"`
MaxBytes int `json:"maxBytes"`
}
// ServerFilesWritePayload writes content to a file in the server data directory.
type ServerFilesWritePayload struct {
OperationMeta
Path string `json:"path"`
Content string `json:"content"`
Create bool `json:"create"`
Encoding string `json:"encoding,omitempty"`
}
// ServerFilesDeletePayload deletes a file or directory relative to data root.
type ServerFilesDeletePayload struct {
OperationMeta
Path string `json:"path"`
}
// ServerFilesArchivePayload creates a tar.gz archive from paths.
type ServerFilesArchivePayload struct {
OperationMeta
Paths []string `json:"paths"`
ArchiveName string `json:"archiveName,omitempty"`
}
// ServerFilesUnarchivePayload extracts a tar.gz archive.
type ServerFilesUnarchivePayload struct {
OperationMeta
ArchivePath string `json:"archivePath"`
DestinationPath string `json:"destinationPath"`
}
// ServerWorldValidatePayload validates a Minecraft world directory.
type ServerWorldValidatePayload struct {
OperationMeta
WorldName string `json:"worldName"`
}
// ServerWorldArchivePayload archives a world directory.
type ServerWorldArchivePayload struct {
OperationMeta
WorldName string `json:"worldName"`
ArchiveID string `json:"archiveId"`
}
// ServerStorageUploadPayload uploads a local file to a presigned URL.
type ServerStorageUploadPayload struct {
OperationMeta
LocalPath string `json:"localPath"`
UploadURL string `json:"uploadUrl"`
}
// ServerStorageDownloadPayload downloads a remote file to a local path.
type ServerStorageDownloadPayload struct {
OperationMeta
DownloadURL string `json:"downloadUrl"`
LocalPath string `json:"localPath"`
}
// ServerWorldReplacePayload replaces a world from a local archive.
type ServerWorldReplacePayload struct {
OperationMeta
WorldName string `json:"worldName"`
ArchivePath string `json:"archivePath"`
}
// ServerAddonInstallPayload downloads and installs an addon file.
type ServerAddonInstallPayload struct {
OperationMeta
DownloadURL string `json:"downloadUrl"`
RelativePath string `json:"relativePath"`
Sha512 string `json:"sha512,omitempty"`
}
// ServerAddonRemovePayload removes an installed addon file.
type ServerAddonRemovePayload struct {
OperationMeta
RelativePath string `json:"relativePath"`
}
// FileEntry describes a file or directory in a list response.
type FileEntry struct {
Name string `json:"name"`
Path string `json:"path"`
IsDir bool `json:"isDir"`
Size int64 `json:"size"`
ModifiedAt string `json:"modifiedAt"`
}
// ServerFilesListResultPayload is the result of a files.list operation.
type ServerFilesListResultPayload struct {
Entries []FileEntry `json:"entries"`
}
// ServerFilesReadResultPayload is the result of a files.read operation.
type ServerFilesReadResultPayload struct {
Path string `json:"path"`
Content string `json:"content"`
Truncated bool `json:"truncated"`
}
// ServerOperationResultPayload reports the outcome of a lifecycle operation.
type ServerOperationResultPayload struct {
OperationMeta
ResultCode string `json:"resultCode"`
Operation string `json:"operation,omitempty"`
Result json.RawMessage `json:"result,omitempty"`
ErrorCode string `json:"errorCode,omitempty"`
ErrorMessage string `json:"errorMessage,omitempty"`
}
// ServerStatePayload reports the observed server state.
type ServerStatePayload struct {
OperationMeta
State string `json:"state"`
ErrorCode string `json:"errorCode,omitempty"`
ErrorMessage string `json:"errorMessage,omitempty"`
}
// Validate checks envelope fields and known message types.
func (e *Envelope) Validate() error {
if e == nil {
return fmt.Errorf("envelope is nil")
}
if e.ProtocolVersion < 1 {
return fmt.Errorf("protocolVersion must be >= 1")
}
if e.ProtocolVersion > CurrentProtocolVersion {
return fmt.Errorf("unsupported protocolVersion %d", e.ProtocolVersion)
}
if strings.TrimSpace(e.MessageID) == "" {
return fmt.Errorf("messageId is required")
}
if strings.TrimSpace(string(e.Type)) == "" {
return fmt.Errorf("type is required")
}
if e.Timestamp.IsZero() {
return fmt.Errorf("timestamp is required")
}
if !e.Timestamp.UTC().Equal(e.Timestamp) {
return fmt.Errorf("timestamp must be UTC")
}
if _, ok := agentToControlTypes[e.Type]; !ok {
if _, ok := controlToAgentTypes[e.Type]; !ok {
return fmt.Errorf("unknown message type: %s", e.Type)
}
}
if len(e.Payload) == 0 {
return fmt.Errorf("payload is required")
}
if !json.Valid(e.Payload) {
return fmt.Errorf("payload must be valid JSON")
}
return nil
}
// IsAgentToControl returns true when the message flows from agent to control plane.
func (t MessageType) IsAgentToControl() bool {
_, ok := agentToControlTypes[t]
return ok
}
// IsControlToAgent returns true when the message flows from control plane to agent.
func (t MessageType) IsControlToAgent() bool {
_, ok := controlToAgentTypes[t]
return ok
}
// DecodeEnvelope parses and validates a JSON envelope.
func DecodeEnvelope(data []byte) (*Envelope, error) {
var env Envelope
if err := json.Unmarshal(data, &env); err != nil {
return nil, fmt.Errorf("decode envelope: %w", err)
}
if err := env.Validate(); err != nil {
return nil, err
}
return &env, nil
}
// NewEnvelope creates a validated outbound envelope.
func NewEnvelope(msgType MessageType, messageID string, payload any) (*Envelope, error) {
raw, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("marshal payload: %w", err)
}
env := &Envelope{
ProtocolVersion: CurrentProtocolVersion,
MessageID: messageID,
Type: msgType,
Timestamp: time.Now().UTC(),
Payload: raw,
}
if err := env.Validate(); err != nil {
return nil, err
}
return env, nil
}