Repository navigation
Expand file tree
/
Copy pathapi.go
More file actions
270 lines (243 loc) · 11 KB
/
Copy pathapi.go
File metadata and controls
270 lines (243 loc) · 11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
// Package api is the wire contract of `crux serve` (Crux Stage 3,
// hivecommons/hive#13536; src/docs/design/crux.md "Stage 3"): the request
// and response types, the routes, the event vocabulary and the address forms
// a client and server agree on. It imports only the standard library, so a
// harness that drives the runtime over the API depends on this package and
// nothing of the runtime itself.
//
// The API is HTTP+JSON under /v1. The server listens on a unix socket by
// default and optionally on TCP; TCP always requires a bearer token
// (Authorization: Bearer <token>), the socket requires one only when the
// server was given one. Streams (log follow, runtime events) are
// text/event-stream, one JSON object per `data:` line, `event:` naming the
// type.
//
// The CLI-shaped endpoints mirror one `crux` CLI verb each and the
// crux.Runtime methods that verb maps onto:
//
// GET /v1/health server is up; agent count
// GET /v1/agents ps Observer.AllStatuses
// POST /v1/agents run Definitions.AddAgent + Lifecycle.Start
// GET /v1/agents/{name} get Observer.GetStatus
// GET /v1/agents/{name}/logs logs Observer.GetOutput / GetBufferOutput / CaptureFullLog
// GET /v1/agents/{name}/logs?follow=1 logs -f Observer.GetBufferOutput, streamed as SSE
// POST /v1/agents/{name}/kick kick Kicker.SendKick
// POST /v1/agents/{name}/stop stop Lifecycle.Stop
// GET /v1/events — runtime events as SSE
//
// {name} is a name or ID; the server resolves it with Observer.ResolveAgent.
//
// The rest of crux.Runtime (hivecommons/hive#14138, stage 3) is in
// runtime.go: one route per family or read, and RuntimeRoutes, the table that
// maps every Runtime method to its route.
package api
import (
"encoding/json"
"time"
)
// Version is the API version every route is prefixed with.
const Version = "v1"
// Routes. Agent routes take the agent's name or ID in place of {name}; use
// AgentPath and friends to build them.
const (
PathHealth = "/" + Version + "/health"
PathAgents = "/" + Version + "/agents"
PathEvents = "/" + Version + "/events"
)
// AgentPath is the route of one agent.
func AgentPath(name string) string { return PathAgents + "/" + name }
// LogsPath is the route of one agent's logs.
func LogsPath(name string) string { return AgentPath(name) + "/logs" }
// KickPath is the route that kicks one agent.
func KickPath(name string) string { return AgentPath(name) + "/kick" }
// StopPath is the route that stops one agent.
func StopPath(name string) string { return AgentPath(name) + "/stop" }
// Log sources, the `source` query parameter of the logs route. They are the
// three readers `crux logs` has: the live pane (default), the ring buffer
// (--buffer) and the whole scrollback (--full).
const (
LogSourcePane = "pane"
LogSourceBuffer = "buffer"
LogSourceFull = "full"
)
// Query parameters of the logs route.
const (
QuerySource = "source"
QueryLines = "n"
QueryFollow = "follow"
)
// DefaultLogLines is how many lines the logs route returns when `n` is
// absent; it matches `crux logs`.
const DefaultLogLines = 100
// Media types.
const (
ContentTypeJSON = "application/json"
ContentTypeEvent = "text/event-stream"
)
// Health is the body of GET /v1/health.
type Health struct {
// Version is the API version the server speaks.
Version string `json:"version"`
// Agents is how many definitions the runtime holds.
Agents int `json:"agents"`
// Background lists the crux.Background loops the server runs in-process
// (the Background method names); empty when it runs none.
Background []string `json:"background,omitempty"`
// HiveCallback is the hive callback endpoint's reachability when the
// server was started with one (`crux serve --hive-callback`); absent
// otherwise. The same state is the hive_callback_unavailable condition
// on the event stream.
HiveCallback *HiveCallback `json:"hive_callback,omitempty"`
// StartedAt is when the server started and UptimeSeconds how long ago
// that was at the time of the answer (#14138 stage 4): a supervisor
// that restarted the server sees the uptime reset.
StartedAt time.Time `json:"started_at"`
UptimeSeconds float64 `json:"uptime_seconds"`
// Tmux is where tmux resolves on PATH, "" when it does not. Every agent
// runs inside tmux, so the route answers 503 (with this body) when empty.
Tmux string `json:"tmux"`
// Error and Code are set only on a non-2xx answer and share their wire
// keys with api.Error, so the 503 body also decodes as the Error that
// Client surfaces in a *StatusError: a caller polling health (the
// supervisor's WaitReady) sees "tmux not found on PATH", not a bare 503:
// WaitReady returns that 503 at once instead of polling to its deadline.
Error string `json:"error,omitempty"`
Code string `json:"code,omitempty"`
}
// HiveCallback is the wire form of the callback client's status.
type HiveCallback struct {
Address string `json:"address"`
Reachable bool `json:"reachable"`
// LastError is the most recent failure, "" when reachable.
LastError string `json:"last_error,omitempty"`
// Since is when the current outage began; absent when reachable.
Since *time.Time `json:"since,omitempty"`
// FailedCalls counts calls that failed closed during the current outage.
FailedCalls int `json:"failed_calls,omitempty"`
// LastSuccess is when a call last reached hive; absent if never.
LastSuccess *time.Time `json:"last_success,omitempty"`
}
// Process is the wire projection of one agent: what `crux ps --json` prints,
// plus the fields a harness needs to decide what to do next (kick refusals,
// pause provenance, the busy condition). It is a subset of crux.Process by
// design: the full snapshot carries engine-internal state that is neither
// stable nor meaningful off-process.
type Process struct {
Name string `json:"name"`
ID string `json:"id"`
State string `json:"state"`
Backend string `json:"backend"`
Model string `json:"model,omitempty"`
PID int `json:"pid,omitempty"`
Paused bool `json:"paused"`
PausedReason string `json:"paused_reason,omitempty"`
PausedTrigger string `json:"paused_trigger,omitempty"`
PausedBy string `json:"paused_by,omitempty"`
StartedAt *time.Time `json:"started_at,omitempty"`
LastKick *time.Time `json:"last_kick,omitempty"`
RestartCount int `json:"restart_count"`
LastRestartReason string `json:"last_restart_reason,omitempty"`
KickRefused bool `json:"kick_refused,omitempty"`
KickRefusalReason string `json:"kick_refusal_reason,omitempty"`
KicksUndeliverable int `json:"kicks_undeliverable,omitempty"`
BusyCondition string `json:"busy_condition,omitempty"`
StartFailureClass string `json:"start_failure_class,omitempty"`
}
// ProcessList is the body of GET /v1/agents, sorted by name.
type ProcessList struct {
Processes []Process `json:"processes"`
}
// RunRequest is the body of POST /v1/agents. Name is required. Definition,
// when present, is an agent definition document in the runtime's own
// configuration shape (the JSON form of crux/config.AgentConfig, which is
// the per-agent overlay hive reads from agents_dir) and is registered with
// Definitions.AddAgent before Lifecycle.Start; Name must then match
// ^[a-z0-9][a-z0-9_-]*$ (at most 64 characters) and not already be defined
// on the server (409 CodeConflict otherwise: the runtime does not replace a
// definition). The server fills the defaults a start depends on (id and
// role from Name, replicas 1, enabled unless the document says otherwise);
// everything else is the caller's. Without a definition, Name (or an agent
// ID) must already be defined on the server. Parsing the portable
// AgentDefinition format is the caller's job, as it is the host's job for
// the CLI.
type RunRequest struct {
Name string `json:"name"`
Definition json.RawMessage `json:"definition,omitempty"`
}
// KickRequest is the body of POST /v1/agents/{name}/kick. Async selects
// Kicker.SendKickAsync; the response's Started then says whether delivery
// began, and GET /v1/agents/{name}/kick reports the dispatch.
type KickRequest struct {
Message string `json:"message"`
Async bool `json:"async,omitempty"`
}
// AgentResponse is the body of a successful run, kick, stop or other
// per-agent action: the resolved agent name and its snapshot after the
// action. Started is set only for an async kick.
type AgentResponse struct {
Agent string `json:"agent"`
Process *Process `json:"process,omitempty"`
Started *bool `json:"started,omitempty"`
}
// Logs is the body of GET /v1/agents/{name}/logs without follow. Lines is
// set for the pane and buffer sources; Header and Content for full.
type Logs struct {
Agent string `json:"agent"`
Source string `json:"source"`
Header string `json:"header,omitempty"`
Lines []string `json:"lines,omitempty"`
Content string `json:"content,omitempty"`
}
// LogChunk is one `data:` record of a followed log stream: the lines
// appended to the ring buffer since the previous chunk.
type LogChunk struct {
Agent string `json:"agent"`
Lines []string `json:"lines"`
}
// Event is one `data:` record of GET /v1/events. Type is one of the Event*
// constants; Agent is empty for runtime-wide events. State carries the new
// state for EventState; Detail is free text for the kick relay. Data is the
// typed payload of the observer events in runtime.go (KickOutcome,
// PauseTransition, ...), absent for the lifecycle events.
type Event struct {
Time time.Time `json:"time"`
Type string `json:"type"`
Agent string `json:"agent,omitempty"`
State string `json:"state,omitempty"`
Detail string `json:"detail,omitempty"`
Data json.RawMessage `json:"data,omitempty"`
}
// Event types. The lifecycle ones are emitted by the server for actions it
// performed; EventState is emitted when the server observes an agent's state
// change for any reason (crash, restart, pause); EventKick relays the
// runtime's kick observer (Kicker.SetKickObserver) with the observer's event
// name in Detail.
const (
EventStarted = "agent.started"
EventStopped = "agent.stopped"
EventKicked = "agent.kicked"
EventState = "agent.state"
EventKick = "agent.kick"
// EventLog is the type of every record on a followed log stream.
EventLog = "agent.log"
)
// Error is the body of every non-2xx response.
type Error struct {
// Error is the message.
Error string `json:"error"`
// Code is a stable machine-readable kind; see the Code* constants.
Code string `json:"code,omitempty"`
}
// Error codes.
const (
CodeUnauthorized = "unauthorized"
CodeBadRequest = "bad_request"
CodeNotFound = "not_found"
// CodeConflict is a run with a definition for a name the server already
// holds; the runtime does not replace definitions.
CodeConflict = "conflict"
CodeRuntime = "runtime_error"
// CodeUnavailable is a server that is up but cannot serve: GET /v1/health
// answers 503 with it when tmux is missing from PATH.
CodeUnavailable = "unavailable"
)