github.com/hashicorp/nomad/api@v0.0.0-20240306165712-3193ac204f65/event_stream_test.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  	"testing"
    10  	"time"
    11  
    12  	"github.com/hashicorp/nomad/api/internal/testutil"
    13  	"github.com/mitchellh/mapstructure"
    14  	"github.com/shoenig/test/must"
    15  )
    16  
    17  func TestTopic_String(t *testing.T) {
    18  	testutil.Parallel(t)
    19  
    20  	testCases := []struct {
    21  		inputTopic     Topic
    22  		expectedOutput string
    23  	}{
    24  		{
    25  			inputTopic:     TopicDeployment,
    26  			expectedOutput: "Deployment",
    27  		},
    28  		{
    29  			inputTopic:     TopicEvaluation,
    30  			expectedOutput: "Evaluation",
    31  		},
    32  		{
    33  			inputTopic:     TopicAllocation,
    34  			expectedOutput: "Allocation",
    35  		},
    36  		{
    37  			inputTopic:     TopicJob,
    38  			expectedOutput: "Job",
    39  		},
    40  		{
    41  			inputTopic:     TopicNode,
    42  			expectedOutput: "Node",
    43  		},
    44  		{
    45  			inputTopic:     TopicNodePool,
    46  			expectedOutput: "NodePool",
    47  		},
    48  		{
    49  			inputTopic:     TopicService,
    50  			expectedOutput: "Service",
    51  		},
    52  		{
    53  			inputTopic:     TopicAll,
    54  			expectedOutput: "*",
    55  		},
    56  	}
    57  
    58  	for _, tc := range testCases {
    59  		t.Run(tc.expectedOutput, func(t *testing.T) {
    60  			actualOutput := tc.inputTopic.String()
    61  			must.Eq(t, tc.expectedOutput, actualOutput)
    62  		})
    63  	}
    64  }
    65  
    66  func TestEvent_Stream(t *testing.T) {
    67  	testutil.Parallel(t)
    68  
    69  	c, s := makeClient(t, nil, nil)
    70  	defer s.Stop()
    71  
    72  	// register job to generate events
    73  	jobs := c.Jobs()
    74  	job := testJob()
    75  	resp2, _, err := jobs.Register(job, nil)
    76  	must.NoError(t, err)
    77  	must.NotNil(t, resp2)
    78  
    79  	// build event stream request
    80  	events := c.EventStream()
    81  	q := &QueryOptions{}
    82  	topics := map[Topic][]string{
    83  		TopicEvaluation: {"*"},
    84  	}
    85  
    86  	ctx, cancel := context.WithCancel(context.Background())
    87  	defer cancel()
    88  
    89  	streamCh, err := events.Stream(ctx, topics, 0, q)
    90  	must.NoError(t, err)
    91  
    92  	select {
    93  	case event := <-streamCh:
    94  		if event.Err != nil {
    95  			must.Unreachable(t, must.Sprintf("unexpected %v", event.Err))
    96  		}
    97  		must.Len(t, 1, event.Events)
    98  		must.Eq(t, "Evaluation", string(event.Events[0].Topic))
    99  	case <-time.After(5 * time.Second):
   100  		must.Unreachable(t, must.Sprint("failed waiting for event stream event"))
   101  	}
   102  }
   103  
   104  func TestEvent_Stream_Err_InvalidQueryParam(t *testing.T) {
   105  	testutil.Parallel(t)
   106  
   107  	c, s := makeClient(t, nil, nil)
   108  	defer s.Stop()
   109  
   110  	// register job to generate events
   111  	jobs := c.Jobs()
   112  	job := testJob()
   113  	resp2, _, err := jobs.Register(job, nil)
   114  	must.NoError(t, err)
   115  	must.NotNil(t, resp2)
   116  
   117  	// build event stream request
   118  	events := c.EventStream()
   119  	q := &QueryOptions{}
   120  	topics := map[Topic][]string{
   121  		TopicEvaluation: {"::*"},
   122  	}
   123  
   124  	ctx, cancel := context.WithCancel(context.Background())
   125  	defer cancel()
   126  
   127  	_, err = events.Stream(ctx, topics, 0, q)
   128  	must.ErrorContains(t, err, "Invalid key value pair")
   129  }
   130  
   131  func TestEvent_Stream_CloseCtx(t *testing.T) {
   132  	testutil.Parallel(t)
   133  
   134  	c, s := makeClient(t, nil, nil)
   135  	defer s.Stop()
   136  
   137  	// register job to generate events
   138  	jobs := c.Jobs()
   139  	job := testJob()
   140  	resp2, _, err := jobs.Register(job, nil)
   141  	must.NoError(t, err)
   142  	must.NotNil(t, resp2)
   143  
   144  	// build event stream request
   145  	events := c.EventStream()
   146  	q := &QueryOptions{}
   147  	topics := map[Topic][]string{
   148  		TopicEvaluation: {"*"},
   149  	}
   150  
   151  	ctx, cancel := context.WithCancel(context.Background())
   152  
   153  	streamCh, err := events.Stream(ctx, topics, 0, q)
   154  	must.NoError(t, err)
   155  
   156  	// cancel the request
   157  	cancel()
   158  
   159  	select {
   160  	case event, ok := <-streamCh:
   161  		must.False(t, ok)
   162  		must.Nil(t, event)
   163  	case <-time.After(5 * time.Second):
   164  		must.Unreachable(t, must.Sprint("failed waiting for event stream event"))
   165  	}
   166  }
   167  
   168  func TestEventStream_PayloadValue(t *testing.T) {
   169  	testutil.Parallel(t)
   170  
   171  	c, s := makeClient(t, nil, func(c *testutil.TestServerConfig) {
   172  		c.DevMode = true
   173  	})
   174  	defer s.Stop()
   175  
   176  	// register job to generate events
   177  	jobs := c.Jobs()
   178  	job := testJob()
   179  	resp2, _, err := jobs.Register(job, nil)
   180  	must.NoError(t, err)
   181  	must.NotNil(t, resp2)
   182  
   183  	// build event stream request
   184  	events := c.EventStream()
   185  	q := &QueryOptions{}
   186  	topics := map[Topic][]string{
   187  		TopicNode: {"*"},
   188  	}
   189  
   190  	ctx, cancel := context.WithCancel(context.Background())
   191  	defer cancel()
   192  
   193  	streamCh, err := events.Stream(ctx, topics, 0, q)
   194  	must.NoError(t, err)
   195  
   196  	select {
   197  	case event := <-streamCh:
   198  		if event.Err != nil {
   199  			must.NoError(t, err)
   200  		}
   201  		for _, e := range event.Events {
   202  			// verify that we get a node
   203  			n, err := e.Node()
   204  			must.NoError(t, err)
   205  			must.UUIDv4(t, n.ID)
   206  
   207  			// perform a raw decoding and look for:
   208  			// - "ID" to make sure that raw decoding is working correctly
   209  			// - "SecretID" to make sure it's not present
   210  			raw := make(map[string]map[string]interface{}, 0)
   211  			cfg := &mapstructure.DecoderConfig{
   212  				Result: &raw,
   213  			}
   214  			dec, err := mapstructure.NewDecoder(cfg)
   215  			must.NoError(t, err)
   216  			must.NoError(t, dec.Decode(e.Payload))
   217  			must.MapContainsKeys(t, raw, []string{"Node"})
   218  			rawNode := raw["Node"]
   219  			must.Eq(t, n.ID, rawNode["ID"].(string))
   220  			must.Eq(t, "", rawNode["SecretID"])
   221  		}
   222  	case <-time.After(5 * time.Second):
   223  		must.Unreachable(t, must.Sprint("failed waiting for event stream event"))
   224  	}
   225  }
   226  
   227  func TestEventStream_PayloadValueHelpers(t *testing.T) {
   228  	testutil.Parallel(t)
   229  
   230  	testCases := []struct {
   231  		desc     string
   232  		event    Event
   233  		input    []byte
   234  		err      string
   235  		expectFn func(t *testing.T, event Event)
   236  	}{
   237  		{
   238  			desc:  "deployment",
   239  			input: []byte(`{"Topic": "Deployment", "Payload": {"Deployment":{"ID":"some-id","JobID":"some-job-id", "TaskGroups": {"tg1": {"RequireProgressBy": "2020-11-05T11:52:54.370774000-05:00"}}}}}`),
   240  			expectFn: func(t *testing.T, event Event) {
   241  				eventTime, err := time.Parse(time.RFC3339, "2020-11-05T11:52:54.370774000-05:00")
   242  				must.NoError(t, err)
   243  				must.Eq(t, TopicDeployment, event.Topic)
   244  
   245  				d, err := event.Deployment()
   246  				must.NoError(t, err)
   247  				must.Eq(t, &Deployment{
   248  					ID:    "some-id",
   249  					JobID: "some-job-id",
   250  					TaskGroups: map[string]*DeploymentState{
   251  						"tg1": {
   252  							RequireProgressBy: eventTime,
   253  						},
   254  					},
   255  				}, d)
   256  			},
   257  		},
   258  		{
   259  			desc:  "evaluation",
   260  			input: []byte(`{"Topic": "Evaluation", "Payload": {"Evaluation":{"ID":"some-id","Namespace":"some-namespace-id"}}}`),
   261  			expectFn: func(t *testing.T, event Event) {
   262  				must.Eq(t, TopicEvaluation, event.Topic)
   263  				eval, err := event.Evaluation()
   264  				must.NoError(t, err)
   265  				must.Eq(t, &Evaluation{
   266  					ID:        "some-id",
   267  					Namespace: "some-namespace-id",
   268  				}, eval)
   269  			},
   270  		},
   271  		{
   272  			desc:  "allocation",
   273  			input: []byte(`{"Topic": "Allocation", "Payload": {"Allocation":{"ID":"some-id","Namespace":"some-namespace-id"}}}`),
   274  			expectFn: func(t *testing.T, event Event) {
   275  				must.Eq(t, TopicAllocation, event.Topic)
   276  				a, err := event.Allocation()
   277  				must.NoError(t, err)
   278  				must.Eq(t, &Allocation{
   279  					ID:        "some-id",
   280  					Namespace: "some-namespace-id",
   281  				}, a)
   282  			},
   283  		},
   284  		{
   285  			input: []byte(`{"Topic": "Job", "Payload": {"Job":{"ID":"some-id","Namespace":"some-namespace-id"}}}`),
   286  			expectFn: func(t *testing.T, event Event) {
   287  				must.Eq(t, TopicJob, event.Topic)
   288  				j, err := event.Job()
   289  				must.NoError(t, err)
   290  				must.Eq(t, &Job{
   291  					ID:        pointerOf("some-id"),
   292  					Namespace: pointerOf("some-namespace-id"),
   293  				}, j)
   294  			},
   295  		},
   296  		{
   297  			desc:  "node",
   298  			input: []byte(`{"Topic": "Node", "Payload": {"Node":{"ID":"some-id","Datacenter":"some-dc-id"}}}`),
   299  			expectFn: func(t *testing.T, event Event) {
   300  				must.Eq(t, TopicNode, event.Topic)
   301  				n, err := event.Node()
   302  				must.NoError(t, err)
   303  				must.Eq(t, &Node{
   304  					ID:         "some-id",
   305  					Datacenter: "some-dc-id",
   306  				}, n)
   307  			},
   308  		},
   309  		{
   310  			desc:  "node_pool",
   311  			input: []byte(`{"Topic":"NodePool","Payload":{"NodePool":{"Description":"prod pool","Name":"prod"}}}`),
   312  			expectFn: func(t *testing.T, event Event) {
   313  				must.Eq(t, TopicNodePool, event.Topic)
   314  				n, err := event.NodePool()
   315  				must.NoError(t, err)
   316  				must.Eq(t, &NodePool{
   317  					Name:        "prod",
   318  					Description: "prod pool",
   319  				}, n)
   320  			},
   321  		},
   322  		{
   323  			desc:  "service",
   324  			input: []byte(`{"Topic": "Service", "Payload": {"Service":{"ID":"some-service-id","Namespace":"some-service-namespace-id","Datacenter":"us-east-1a"}}}`),
   325  			expectFn: func(t *testing.T, event Event) {
   326  				must.Eq(t, TopicService, event.Topic)
   327  				a, err := event.Service()
   328  				must.NoError(t, err)
   329  				must.Eq(t, "us-east-1a", a.Datacenter)
   330  				must.Eq(t, "some-service-id", a.ID)
   331  				must.Eq(t, "some-service-namespace-id", a.Namespace)
   332  			},
   333  		},
   334  	}
   335  
   336  	for _, tc := range testCases {
   337  		t.Run(tc.desc, func(t *testing.T) {
   338  			var out Event
   339  			err := json.Unmarshal(tc.input, &out)
   340  			must.NoError(t, err)
   341  			tc.expectFn(t, out)
   342  		})
   343  	}
   344  }