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 }