github.com/hashicorp/nomad/api@v0.0.0-20240306165712-3193ac204f65/event_stream.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package api 5 6 import ( 7 "context" 8 "encoding/json" 9 "fmt" 10 "strconv" 11 "time" 12 13 "github.com/mitchellh/mapstructure" 14 ) 15 16 const ( 17 TopicDeployment Topic = "Deployment" 18 TopicEvaluation Topic = "Evaluation" 19 TopicAllocation Topic = "Allocation" 20 TopicJob Topic = "Job" 21 TopicNode Topic = "Node" 22 TopicNodePool Topic = "NodePool" 23 TopicService Topic = "Service" 24 TopicAll Topic = "*" 25 ) 26 27 // Events is a set of events for a corresponding index. Events returned for the 28 // index depend on which topics are subscribed to when a request is made. 29 type Events struct { 30 Index uint64 31 Events []Event 32 Err error 33 } 34 35 // Topic is an event Topic 36 type Topic string 37 38 // String is a convenience function which returns the topic as a string type 39 // representation. 40 func (t Topic) String() string { return string(t) } 41 42 // Event holds information related to an event that occurred in Nomad. 43 // The Payload is a hydrated object related to the Topic 44 type Event struct { 45 Topic Topic 46 Type string 47 Key string 48 FilterKeys []string 49 Index uint64 50 Payload map[string]interface{} 51 } 52 53 // Deployment returns a Deployment struct from a given event payload. If the 54 // Event Topic is Deployment this will return a valid Deployment 55 func (e *Event) Deployment() (*Deployment, error) { 56 out, err := e.decodePayload() 57 if err != nil { 58 return nil, err 59 } 60 return out.Deployment, nil 61 } 62 63 // Evaluation returns a Evaluation struct from a given event payload. If the 64 // Event Topic is Evaluation this will return a valid Evaluation 65 func (e *Event) Evaluation() (*Evaluation, error) { 66 out, err := e.decodePayload() 67 if err != nil { 68 return nil, err 69 } 70 return out.Evaluation, nil 71 } 72 73 // Allocation returns a Allocation struct from a given event payload. If the 74 // Event Topic is Allocation this will return a valid Allocation. 75 func (e *Event) Allocation() (*Allocation, error) { 76 out, err := e.decodePayload() 77 if err != nil { 78 return nil, err 79 } 80 return out.Allocation, nil 81 } 82 83 // Job returns a Job struct from a given event payload. If the 84 // Event Topic is Job this will return a valid Job. 85 func (e *Event) Job() (*Job, error) { 86 out, err := e.decodePayload() 87 if err != nil { 88 return nil, err 89 } 90 return out.Job, nil 91 } 92 93 // Node returns a Node struct from a given event payload. If the 94 // Event Topic is Node this will return a valid Node. 95 func (e *Event) Node() (*Node, error) { 96 out, err := e.decodePayload() 97 if err != nil { 98 return nil, err 99 } 100 return out.Node, nil 101 } 102 103 // NodePool returns a NodePool struct from a given event payload. If the Event 104 // Topic is NodePool this will return a valid NodePool. 105 func (e *Event) NodePool() (*NodePool, error) { 106 out, err := e.decodePayload() 107 if err != nil { 108 return nil, err 109 } 110 return out.NodePool, nil 111 } 112 113 // Service returns a ServiceRegistration struct from a given event payload. If 114 // the Event Topic is Service this will return a valid ServiceRegistration. 115 func (e *Event) Service() (*ServiceRegistration, error) { 116 out, err := e.decodePayload() 117 if err != nil { 118 return nil, err 119 } 120 return out.Service, nil 121 } 122 123 type eventPayload struct { 124 Allocation *Allocation `mapstructure:"Allocation"` 125 Deployment *Deployment `mapstructure:"Deployment"` 126 Evaluation *Evaluation `mapstructure:"Evaluation"` 127 Job *Job `mapstructure:"Job"` 128 Node *Node `mapstructure:"Node"` 129 NodePool *NodePool `mapstructure:"NodePool"` 130 Service *ServiceRegistration `mapstructure:"Service"` 131 } 132 133 func (e *Event) decodePayload() (*eventPayload, error) { 134 var out eventPayload 135 cfg := &mapstructure.DecoderConfig{ 136 Result: &out, 137 DecodeHook: mapstructure.StringToTimeHookFunc(time.RFC3339), 138 } 139 140 dec, err := mapstructure.NewDecoder(cfg) 141 if err != nil { 142 return nil, err 143 } 144 145 if err := dec.Decode(e.Payload); err != nil { 146 return nil, err 147 } 148 149 return &out, nil 150 } 151 152 // IsHeartbeat specifies if the event is an empty heartbeat used to 153 // keep a connection alive. 154 func (e *Events) IsHeartbeat() bool { 155 return e.Index == 0 && len(e.Events) == 0 156 } 157 158 // EventStream is used to stream events from Nomad 159 type EventStream struct { 160 client *Client 161 } 162 163 // EventStream returns a handle to the Events endpoint 164 func (c *Client) EventStream() *EventStream { 165 return &EventStream{client: c} 166 } 167 168 // Stream establishes a new subscription to Nomad's event stream and streams 169 // results back to the returned channel. 170 func (e *EventStream) Stream(ctx context.Context, topics map[Topic][]string, index uint64, q *QueryOptions) (<-chan *Events, error) { 171 r, err := e.client.newRequest("GET", "/v1/event/stream") 172 if err != nil { 173 return nil, err 174 } 175 q = q.WithContext(ctx) 176 if q.Params == nil { 177 q.Params = map[string]string{} 178 } 179 q.Params["index"] = strconv.FormatUint(index, 10) 180 r.setQueryOptions(q) 181 182 // Build topic query params 183 for topic, keys := range topics { 184 for _, k := range keys { 185 r.params.Add("topic", fmt.Sprintf("%s:%s", topic, k)) 186 } 187 } 188 189 _, resp, err := requireOK(e.client.doRequest(r)) //nolint:bodyclose 190 191 if err != nil { 192 return nil, err 193 } 194 195 eventsCh := make(chan *Events, 10) 196 go func() { 197 defer resp.Body.Close() 198 defer close(eventsCh) 199 200 dec := json.NewDecoder(resp.Body) 201 202 for ctx.Err() == nil { 203 // Decode next newline delimited json of events 204 var events Events 205 if err := dec.Decode(&events); err != nil { 206 // set error and fallthrough to 207 // select eventsCh 208 events = Events{Err: err} 209 } 210 if events.Err == nil && events.IsHeartbeat() { 211 continue 212 } 213 214 select { 215 case <-ctx.Done(): 216 return 217 case eventsCh <- &events: 218 } 219 } 220 }() 221 222 return eventsCh, nil 223 }