feat(protocol): job messages, protocol version and job model (#7)
This commit is contained in:
+106
-15
@@ -1,33 +1,66 @@
|
||||
// Package protocol defines the messages agents and the master exchange over
|
||||
// their WebSocket connection. Every message is an Envelope whose Payload is
|
||||
// one of the payload types below, selected by Type.
|
||||
//
|
||||
// Both sides close the connection on a message they cannot parse (bad JSON,
|
||||
// or a known Type whose payload does not decode). A message with an unknown
|
||||
// Type is logged and ignored, so a newer peer can add message types without
|
||||
// breaking an older one.
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Version is the protocol version this build speaks. Agents send it in
|
||||
// Hello. Phase-1 agents do not send the field, which reads as 0.
|
||||
const Version = 2
|
||||
|
||||
// MinJobVersion is the lowest agent protocol version the master sends jobs to.
|
||||
const MinJobVersion = 2
|
||||
|
||||
// MaxJobOutput caps the job output an agent keeps and the master stores, in
|
||||
// bytes. Only the tail is kept. With JSON escaping (worst case 6 bytes per
|
||||
// byte) a JobResult carrying this much output still fits the 1 MiB read limit.
|
||||
const MaxJobOutput = 128 << 10
|
||||
|
||||
// Message types sent from agent to master.
|
||||
const (
|
||||
TypeHello = "hello"
|
||||
TypeSnapshot = "snapshot"
|
||||
TypeUpsert = "upsert"
|
||||
TypeRemove = "remove"
|
||||
TypeHello = "hello"
|
||||
TypeSnapshot = "snapshot"
|
||||
TypeUpsert = "upsert"
|
||||
TypeRemove = "remove"
|
||||
TypeJobProgress = "job.progress"
|
||||
TypeJobResult = "job.result"
|
||||
)
|
||||
|
||||
// Envelope wraps every message on the wire.
|
||||
// Message types sent from master to agent.
|
||||
const (
|
||||
TypeJobStart = "job.start"
|
||||
TypeJobAck = "job.ack"
|
||||
)
|
||||
|
||||
// Envelope wraps every message on the wire. ID is the job ID (decimal) on
|
||||
// job.* messages and empty on inventory messages.
|
||||
type Envelope struct {
|
||||
Type string `json:"type"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Payload json.RawMessage `json:"payload,omitempty"`
|
||||
}
|
||||
|
||||
// Hello is the first message an agent sends on every connection.
|
||||
type Hello struct {
|
||||
AgentVersion string `json:"agent_version"`
|
||||
Hostname string `json:"hostname"`
|
||||
SwarmManager bool `json:"swarm_manager"`
|
||||
SwarmClusterID string `json:"swarm_cluster_id,omitempty"`
|
||||
AgentVersion string `json:"agent_version"`
|
||||
ProtocolVersion int `json:"protocol_version,omitempty"`
|
||||
Hostname string `json:"hostname"`
|
||||
SwarmManager bool `json:"swarm_manager"`
|
||||
SwarmClusterID string `json:"swarm_cluster_id,omitempty"`
|
||||
// KnownJobs lists every job ID the agent holds: queued, running, or
|
||||
// finished but not yet acknowledged. The master fails its queued and
|
||||
// running jobs for this agent that are not listed ("lost").
|
||||
KnownJobs []string `json:"known_jobs,omitempty"`
|
||||
}
|
||||
|
||||
// Kind says how a service is deployed.
|
||||
@@ -47,12 +80,16 @@ type Service struct {
|
||||
ComposeProject string `json:"compose_project,omitempty"`
|
||||
ComposeService string `json:"compose_service,omitempty"`
|
||||
WorkingDir string `json:"working_dir,omitempty"`
|
||||
SwarmService string `json:"swarm_service,omitempty"`
|
||||
ContainerName string `json:"container_name,omitempty"`
|
||||
Image string `json:"image"`
|
||||
Digest string `json:"digest,omitempty"`
|
||||
Status string `json:"status"`
|
||||
Self bool `json:"self,omitempty"`
|
||||
// ConfigFiles is the raw com.docker.compose.project.config_files label:
|
||||
// absolute host paths joined by ",". Kept as a string so Service stays
|
||||
// comparable with ==.
|
||||
ConfigFiles string `json:"config_files,omitempty"`
|
||||
SwarmService string `json:"swarm_service,omitempty"`
|
||||
ContainerName string `json:"container_name,omitempty"`
|
||||
Image string `json:"image"`
|
||||
Digest string `json:"digest,omitempty"`
|
||||
Status string `json:"status"`
|
||||
Self bool `json:"self,omitempty"`
|
||||
}
|
||||
|
||||
// Key identifies the service within one agent's report. It is stable across
|
||||
@@ -83,6 +120,50 @@ type Remove struct {
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
// JobStart asks the agent to deploy Ref to one service. Ref is "repo:tag";
|
||||
// the agent resolves the tag's current digest and deploys "repo:tag@digest".
|
||||
// The agent reads the Compose working directory and config files from its
|
||||
// own Docker daemon, not from this message.
|
||||
type JobStart struct {
|
||||
Kind Kind `json:"kind"`
|
||||
ComposeProject string `json:"compose_project,omitempty"`
|
||||
ComposeService string `json:"compose_service,omitempty"`
|
||||
SwarmClusterID string `json:"swarm_cluster_id,omitempty"`
|
||||
SwarmService string `json:"swarm_service,omitempty"`
|
||||
Ref string `json:"ref"`
|
||||
}
|
||||
|
||||
// JobProgress carries output produced since the previous JobProgress for the
|
||||
// same job. The first JobProgress of a job (Output may be empty) tells the
|
||||
// master the job is running.
|
||||
type JobProgress struct {
|
||||
Output string `json:"output"`
|
||||
}
|
||||
|
||||
// Job result statuses.
|
||||
const (
|
||||
ResultSucceeded = "succeeded"
|
||||
ResultFailed = "failed"
|
||||
)
|
||||
|
||||
// JobResult is a job's outcome. The agent keeps it until the master sends
|
||||
// JobAck for the same ID, and resends it after every reconnect until then.
|
||||
type JobResult struct {
|
||||
Status string `json:"status"` // ResultSucceeded or ResultFailed
|
||||
Ref string `json:"ref,omitempty"` // deployed "repo:tag@digest", set on success
|
||||
Error string `json:"error,omitempty"`
|
||||
// Output is the job's complete output, tail-capped at MaxJobOutput. It
|
||||
// replaces whatever the master assembled from JobProgress messages.
|
||||
Output string `json:"output"`
|
||||
OutputTruncated bool `json:"output_truncated,omitempty"`
|
||||
StartedAt time.Time `json:"started_at"`
|
||||
FinishedAt time.Time `json:"finished_at"`
|
||||
}
|
||||
|
||||
// JobAck tells the agent the master has stored the JobResult with the same
|
||||
// ID. It has no fields.
|
||||
type JobAck struct{}
|
||||
|
||||
// Encode wraps payload in an Envelope of the given type.
|
||||
func Encode(typ string, payload any) (Envelope, error) {
|
||||
raw, err := json.Marshal(payload)
|
||||
@@ -92,6 +173,16 @@ func Encode(typ string, payload any) (Envelope, error) {
|
||||
return Envelope{Type: typ, Payload: raw}, nil
|
||||
}
|
||||
|
||||
// EncodeJob wraps a job message's payload in an Envelope carrying the job ID.
|
||||
func EncodeJob(typ, jobID string, payload any) (Envelope, error) {
|
||||
env, err := Encode(typ, payload)
|
||||
if err != nil {
|
||||
return Envelope{}, err
|
||||
}
|
||||
env.ID = jobID
|
||||
return env, nil
|
||||
}
|
||||
|
||||
// Decode unpacks env's payload into T.
|
||||
func Decode[T any](env Envelope) (T, error) {
|
||||
var v T
|
||||
|
||||
Reference in New Issue
Block a user