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  }