github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/command/agent/event_endpoint_test.go (about) 1 package agent 2 3 import ( 4 "context" 5 "fmt" 6 "net/http" 7 "net/http/httptest" 8 "net/url" 9 "strings" 10 "testing" 11 "time" 12 13 "github.com/hashicorp/nomad/api" 14 "github.com/hashicorp/nomad/ci" 15 "github.com/hashicorp/nomad/helper/uuid" 16 "github.com/hashicorp/nomad/nomad/mock" 17 "github.com/hashicorp/nomad/nomad/structs" 18 "github.com/hashicorp/nomad/testutil" 19 "github.com/stretchr/testify/assert" 20 "github.com/stretchr/testify/require" 21 ) 22 23 type testEvent struct { 24 ID string 25 } 26 27 func TestEventStream(t *testing.T) { 28 ci.Parallel(t) 29 30 httpTest(t, nil, func(s *TestAgent) { 31 ctx, cancel := context.WithCancel(context.Background()) 32 req, err := http.NewRequestWithContext(ctx, "GET", "/v1/event/stream", nil) 33 require.Nil(t, err) 34 resp := httptest.NewRecorder() 35 36 respErrCh := make(chan error) 37 go func() { 38 _, err = s.Server.EventStream(resp, req) 39 respErrCh <- err 40 assert.NoError(t, err) 41 }() 42 43 pub, err := s.Agent.server.State().EventBroker() 44 require.NoError(t, err) 45 pub.Publish(&structs.Events{Index: 100, Events: []structs.Event{{Payload: testEvent{ID: "123"}}}}) 46 47 testutil.WaitForResult(func() (bool, error) { 48 got := resp.Body.String() 49 want := `{"ID":"123"}` 50 if strings.Contains(got, want) { 51 return true, nil 52 } 53 54 return false, fmt.Errorf("missing expected json, got: %v, want: %v", got, want) 55 }, func(err error) { 56 cancel() 57 require.Fail(t, err.Error()) 58 }) 59 60 // wait for response to close to prevent race between subscription 61 // shutdown and server shutdown returning subscription closed by server err 62 cancel() 63 select { 64 case err := <-respErrCh: 65 require.Nil(t, err) 66 case <-time.After(1 * time.Second): 67 require.Fail(t, "waiting for request cancellation") 68 } 69 }) 70 } 71 72 func TestEventStream_NamespaceQuery(t *testing.T) { 73 ci.Parallel(t) 74 75 httpTest(t, nil, func(s *TestAgent) { 76 ctx, cancel := context.WithCancel(context.Background()) 77 defer cancel() 78 79 req, err := http.NewRequestWithContext(ctx, "GET", "/v1/event/stream?namespace=foo", nil) 80 require.Nil(t, err) 81 resp := httptest.NewRecorder() 82 83 respErrCh := make(chan error) 84 go func() { 85 _, err = s.Server.EventStream(resp, req) 86 respErrCh <- err 87 assert.NoError(t, err) 88 }() 89 90 pub, err := s.Agent.server.State().EventBroker() 91 require.NoError(t, err) 92 93 badID := uuid.Generate() 94 pub.Publish(&structs.Events{Index: 100, Events: []structs.Event{{Namespace: "bar", Payload: testEvent{ID: badID}}}}) 95 pub.Publish(&structs.Events{Index: 101, Events: []structs.Event{{Namespace: "foo", Payload: testEvent{ID: "456"}}}}) 96 97 testutil.WaitForResult(func() (bool, error) { 98 got := resp.Body.String() 99 want := `"Namespace":"foo"` 100 if strings.Contains(got, badID) { 101 return false, fmt.Errorf("expected non matching namespace to be filtered, got:%v", got) 102 } 103 if strings.Contains(got, want) { 104 return true, nil 105 } 106 107 return false, fmt.Errorf("missing expected json, got: %v, want: %v", got, want) 108 }, func(err error) { 109 require.Fail(t, err.Error()) 110 }) 111 112 // wait for response to close to prevent race between subscription 113 // shutdown and server shutdown returning subscription closed by server err 114 cancel() 115 select { 116 case err := <-respErrCh: 117 require.Nil(t, err) 118 case <-time.After(1 * time.Second): 119 require.Fail(t, "waiting for request cancellation") 120 } 121 }) 122 } 123 124 func TestEventStream_QueryParse(t *testing.T) { 125 ci.Parallel(t) 126 127 cases := []struct { 128 desc string 129 query string 130 want map[structs.Topic][]string 131 wantErr bool 132 }{ 133 { 134 desc: "all topics and keys specified", 135 query: "?topic=*:*", 136 want: map[structs.Topic][]string{ 137 "*": {"*"}, 138 }, 139 }, 140 { 141 desc: "all topics and keys inferred", 142 query: "", 143 want: map[structs.Topic][]string{ 144 "*": {"*"}, 145 }, 146 }, 147 { 148 desc: "invalid key value formatting", 149 query: "?topic=NodeDrain:*:*", 150 wantErr: true, 151 }, 152 { 153 desc: "Infer wildcard if absent", 154 query: "?topic=NodeDrain", 155 wantErr: false, 156 want: map[structs.Topic][]string{ 157 "NodeDrain": {"*"}, 158 }, 159 }, 160 { 161 desc: "single topic and key", 162 query: "?topic=NodeDrain:*", 163 want: map[structs.Topic][]string{ 164 "NodeDrain": {"*"}, 165 }, 166 }, 167 { 168 desc: "single topic multiple keys", 169 query: "?topic=NodeDrain:*&topic=NodeDrain:3caace09-f1f4-4d23-b37a-9ab5eb75069d", 170 want: map[structs.Topic][]string{ 171 "NodeDrain": { 172 "*", 173 "3caace09-f1f4-4d23-b37a-9ab5eb75069d", 174 }, 175 }, 176 }, 177 { 178 desc: "multiple topics", 179 query: "?topic=NodeRegister:*&topic=NodeDrain:3caace09-f1f4-4d23-b37a-9ab5eb75069d", 180 want: map[structs.Topic][]string{ 181 "NodeDrain": { 182 "3caace09-f1f4-4d23-b37a-9ab5eb75069d", 183 }, 184 "NodeRegister": { 185 "*", 186 }, 187 }, 188 }, 189 } 190 191 for _, tc := range cases { 192 t.Run(tc.desc, func(t *testing.T) { 193 raw := fmt.Sprintf("http://localhost:80/v1/events%s", tc.query) 194 req, err := url.Parse(raw) 195 require.NoError(t, err) 196 197 got, err := parseEventTopics(req.Query()) 198 if tc.wantErr { 199 require.Error(t, err) 200 return 201 } 202 require.NoError(t, err) 203 require.Equal(t, tc.want, got) 204 }) 205 } 206 } 207 208 func TestHTTP_Alloc_Port_Response(t *testing.T) { 209 ci.Parallel(t) 210 211 httpTest(t, nil, func(srv *TestAgent) { 212 client := srv.Client() 213 defer srv.Shutdown() 214 defer client.Close() 215 216 testutil.WaitForLeader(t, srv.Agent.RPC) 217 testutil.WaitForClient(t, srv.Agent.Client().RPC, srv.Agent.Client().NodeID(), srv.Agent.Client().Region()) 218 219 job := MockRunnableJob() 220 221 resp, _, err := client.Jobs().Register(job, nil) 222 require.NoError(t, err) 223 require.NotEmpty(t, resp.EvalID) 224 225 alloc := mock.Alloc() 226 alloc.Job = ApiJobToStructJob(job) 227 alloc.JobID = *job.ID 228 alloc.NodeID = srv.client.NodeID() 229 230 require.Nil(t, srv.server.State().UpsertJobSummary(101, mock.JobSummary(alloc.JobID))) 231 require.Nil(t, srv.server.State().UpsertAllocs(structs.MsgTypeTestSetup, 102, []*structs.Allocation{alloc})) 232 233 running := false 234 testutil.WaitForResult(func() (bool, error) { 235 upsertResult, stateErr := srv.server.State().AllocByID(nil, alloc.ID) 236 if stateErr != nil { 237 return false, stateErr 238 } 239 if upsertResult.ClientStatus == structs.AllocClientStatusRunning { 240 running = true 241 return true, nil 242 } 243 return false, nil 244 }, func(err error) { 245 require.NoError(t, err, "allocation query failed") 246 }) 247 248 require.True(t, running) 249 250 topics := map[api.Topic][]string{ 251 api.TopicAllocation: {*job.ID}, 252 } 253 254 ctx, cancel := context.WithCancel(context.Background()) 255 defer cancel() 256 257 events := client.EventStream() 258 streamCh, err := events.Stream(ctx, topics, 1, nil) 259 require.NoError(t, err) 260 261 var allocEvents []api.Event 262 // gather job alloc events 263 go func() { 264 for { 265 select { 266 case event, ok := <-streamCh: 267 if !ok { 268 return 269 } 270 if event.IsHeartbeat() { 271 continue 272 } 273 allocEvents = append(allocEvents, event.Events...) 274 case <-time.After(10 * time.Second): 275 require.Fail(t, "failed waiting for event stream event") 276 } 277 } 278 }() 279 280 var networkResource *api.NetworkResource 281 testutil.WaitForResult(func() (bool, error) { 282 for _, e := range allocEvents { 283 if e.Type == structs.TypeAllocationUpdated { 284 eventAlloc, err := e.Allocation() 285 if err != nil { 286 return false, err 287 } 288 if len(eventAlloc.AllocatedResources.Tasks["web"].Networks) == 0 { 289 return false, nil 290 } 291 networkResource = eventAlloc.AllocatedResources.Tasks["web"].Networks[0] 292 if networkResource.ReservedPorts[0].Value == 5000 { 293 return true, nil 294 } 295 } 296 } 297 return false, nil 298 }, func(e error) { 299 require.NoError(t, err) 300 }) 301 302 require.NotNil(t, networkResource) 303 require.Equal(t, 5000, networkResource.ReservedPorts[0].Value) 304 require.NotEqual(t, 0, networkResource.DynamicPorts[0].Value) 305 }) 306 }