235 lines
7.7 KiB
Go
235 lines
7.7 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"
|
|
TypeServerFilesUploadDone MessageType = "server.files.upload.complete"
|
|
TypeServerWorldValidate MessageType = "server.world.validate"
|
|
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: {},
|
|
TypeServerFilesUploadDone: {},
|
|
TypeServerWorldValidate: {},
|
|
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
|
|
}
|
|
|
|
// ServerOperationResultPayload reports the outcome of a lifecycle operation.
|
|
type ServerOperationResultPayload struct {
|
|
OperationMeta
|
|
ResultCode string `json:"resultCode"`
|
|
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
|
|
}
|