github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/e2e/servicediscovery/service_discovery_test.go (about) 1 package servicediscovery 2 3 import ( 4 "context" 5 "reflect" 6 "strings" 7 "testing" 8 "time" 9 10 "github.com/hashicorp/nomad/api" 11 "github.com/hashicorp/nomad/e2e/e2eutil" 12 "github.com/hashicorp/nomad/helper/uuid" 13 "github.com/stretchr/testify/require" 14 "golang.org/x/exp/slices" 15 ) 16 17 const ( 18 jobNomadProvider = "./input/nomad_provider.nomad" 19 jobConsulProvider = "./input/consul_provider.nomad" 20 jobMultiProvider = "./input/multi_provider.nomad" 21 jobSimpleLBReplicas = "./input/simple_lb_replicas.nomad" 22 jobSimpleLBClients = "./input/simple_lb_clients.nomad" 23 jobChecksHappy = "./input/checks_happy.nomad" 24 jobChecksSad = "./input/checks_sad.nomad" 25 ) 26 27 const ( 28 defaultWaitForTime = 5 * time.Second 29 defaultTickTime = 200 * time.Millisecond 30 ) 31 32 // TestServiceDiscovery runs a number of tests which exercise Nomads service 33 // discovery functionality. It does not test subsystems of service discovery 34 // such as Consul Connect, which have their own test suite. 35 func TestServiceDiscovery(t *testing.T) { 36 37 // Wait until we have a usable cluster before running the tests. 38 nomadClient := e2eutil.NomadClient(t) 39 e2eutil.WaitForLeader(t, nomadClient) 40 e2eutil.WaitForNodesReady(t, nomadClient, 1) 41 42 // Run our test cases. 43 t.Run("TestServiceDiscovery_MultiProvider", testMultiProvider) 44 t.Run("TestServiceDiscovery_UpdateProvider", testUpdateProvider) 45 t.Run("TestServiceDiscovery_SimpleLoadBalancing", testSimpleLoadBalancing) 46 t.Run("TestServiceDiscovery_ChecksHappy", testChecksHappy) 47 t.Run("TestServiceDiscovery_ChecksSad", testChecksSad) 48 } 49 50 // testMultiProvider tests service discovery where multi providers are used 51 // within a single job. 52 func testMultiProvider(t *testing.T) { 53 54 nomadClient := e2eutil.NomadClient(t) 55 consulClient := e2eutil.ConsulClient(t) 56 57 // Generate our job ID which will be used for the entire test. 58 jobID := "service-discovery-multi-provider-" + uuid.Short() 59 jobIDs := []string{jobID} 60 61 // Defer a cleanup function to remove the job. This will trigger if the 62 // test fails, unless the cancel function is called. 63 ctx, cancel := context.WithCancel(context.Background()) 64 defer e2eutil.CleanupJobsAndGCWithContext(t, ctx, &jobIDs) 65 66 // Register the job which contains two groups, each with a single service 67 // that use different providers. 68 allocStubs := e2eutil.RegisterAndWaitForAllocs(t, nomadClient, jobMultiProvider, jobID, "") 69 require.Len(t, allocStubs, 2) 70 71 // We need to understand which allocation belongs to which group so we can 72 // test the service registrations properly. 73 var nomadProviderAllocID, consulProviderAllocID string 74 75 for _, allocStub := range allocStubs { 76 switch allocStub.TaskGroup { 77 case "service_discovery": 78 consulProviderAllocID = allocStub.ID 79 case "service_discovery_secondary": 80 nomadProviderAllocID = allocStub.ID 81 default: 82 t.Fatalf("unknown task group allocation found: %q", allocStub.TaskGroup) 83 } 84 } 85 86 require.NotEmpty(t, nomadProviderAllocID) 87 require.NotEmpty(t, consulProviderAllocID) 88 89 // Services are registered on by the client, so we need to wait for the 90 // alloc to be running before continue safely. 91 e2eutil.WaitForAllocsRunning(t, nomadClient, []string{nomadProviderAllocID, consulProviderAllocID}) 92 93 // Lookup the service registration in Nomad and assert this matches what we 94 // expected. 95 expectedNomadService := api.ServiceRegistration{ 96 ServiceName: "http-api-nomad", 97 Namespace: api.DefaultNamespace, 98 Datacenter: "dc1", 99 JobID: jobID, 100 AllocID: nomadProviderAllocID, 101 Tags: []string{"foo", "bar"}, 102 } 103 requireEventuallyNomadService(t, &expectedNomadService, "") 104 105 // Lookup the service registration in Consul and assert this matches what 106 // we expected. 107 require.Eventually(t, func() bool { 108 consulServices, _, err := consulClient.Catalog().Service("http-api", "", nil) 109 if err != nil { 110 return false 111 } 112 113 // Perform the checks. 114 if len(consulServices) != 1 { 115 return false 116 } 117 if consulServices[0].ServiceName != "http-api" { 118 return false 119 } 120 if !strings.Contains(consulServices[0].ServiceID, consulProviderAllocID) { 121 return false 122 } 123 if !reflect.DeepEqual(consulServices[0].ServiceTags, []string{"foo", "bar"}) { 124 return false 125 } 126 return reflect.DeepEqual(consulServices[0].ServiceMeta, map[string]string{"external-source": "nomad"}) 127 }, defaultWaitForTime, defaultTickTime) 128 129 // Register a "modified" job which removes the second task group and 130 // therefore the service registration that is within Nomad. 131 allocStubs = e2eutil.RegisterAndWaitForAllocs(t, nomadClient, jobConsulProvider, jobID, "") 132 require.Len(t, allocStubs, 2) 133 134 // Check the allocations have the expected. 135 require.Eventually(t, func() bool { 136 allocStubs, _, err := nomadClient.Jobs().Allocations(jobID, true, nil) 137 if err != nil { 138 return false 139 } 140 if len(allocStubs) != 2 { 141 return false 142 } 143 144 var correctStatus bool 145 146 for _, allocStub := range allocStubs { 147 switch allocStub.TaskGroup { 148 case "service_discovery": 149 correctStatus = correctStatus || api.AllocClientStatusRunning == allocStub.ClientStatus 150 case "service_discovery_secondary": 151 correctStatus = correctStatus || api.AllocClientStatusComplete == allocStub.ClientStatus 152 default: 153 t.Fatalf("unknown task group allocation found: %q", allocStub.TaskGroup) 154 } 155 } 156 return correctStatus 157 }, defaultWaitForTime, defaultTickTime) 158 159 // We should now have zero service registrations for the given serviceName 160 // within Nomad. 161 require.Eventually(t, func() bool { 162 services, _, err := nomadClient.Services().Get("http-api-nomad", nil) 163 if err != nil { 164 return false 165 } 166 return len(services) == 0 167 }, defaultWaitForTime, defaultTickTime) 168 169 // The service registration should still exist within Consul. 170 require.Eventually(t, func() bool { 171 consulServices, _, err := consulClient.Catalog().Service("http-api", "", nil) 172 if err != nil { 173 return false 174 } 175 176 // Perform the checks. 177 if len(consulServices) != 1 { 178 return false 179 } 180 if consulServices[0].ServiceName != "http-api" { 181 return false 182 } 183 if !strings.Contains(consulServices[0].ServiceID, consulProviderAllocID) { 184 return false 185 } 186 if !reflect.DeepEqual(consulServices[0].ServiceTags, []string{"foo", "bar"}) { 187 return false 188 } 189 return reflect.DeepEqual(consulServices[0].ServiceMeta, map[string]string{"external-source": "nomad"}) 190 }, defaultWaitForTime, defaultTickTime) 191 192 // Purge the job and ensure the service is removed. If this completes 193 // successfully, cancel the deferred cleanup. 194 e2eutil.CleanupJobsAndGC(t, &jobIDs)() 195 cancel() 196 197 // Ensure the service has now been removed from Consul. Wrap this in an 198 // eventual as Consul updates are a-sync. 199 require.Eventually(t, func() bool { 200 consulServices, _, err := consulClient.Catalog().Service("http-api", "", nil) 201 if err != nil { 202 return false 203 } 204 return len(consulServices) == 0 205 }, defaultWaitForTime, defaultTickTime) 206 } 207 208 // testUpdateProvider tests updating the service provider within a running job 209 // to ensure the backend providers are updated as expected. 210 func testUpdateProvider(t *testing.T) { 211 212 nomadClient := e2eutil.NomadClient(t) 213 const serviceName = "http-api" 214 215 // Generate our job ID which will be used for the entire test. 216 jobID := "service-discovery-update-provider-" + uuid.Short() 217 jobIDs := []string{jobID} 218 219 // Defer a cleanup function to remove the job. This will trigger if the 220 // test fails, unless the cancel function is called. 221 ctx, cancel := context.WithCancel(context.Background()) 222 defer e2eutil.CleanupJobsAndGCWithContext(t, ctx, &jobIDs) 223 224 // We want to capture this for use outside the test func routine. 225 var nomadProviderAllocID string 226 227 // Capture the Nomad mini-test as a function, so we can call this twice 228 // during this test. 229 nomadServiceTestFn := func() { 230 231 // Register the job and get our allocation ID which we can use for later 232 // tests. 233 allocStubs := e2eutil.RegisterAndWaitForAllocs(t, nomadClient, jobNomadProvider, jobID, "") 234 require.Len(t, allocStubs, 1) 235 nomadProviderAllocID = allocStubs[0].ID 236 237 // Services are registered on by the client, so we need to wait for the 238 // alloc to be running before continue safely. 239 e2eutil.WaitForAllocRunning(t, nomadClient, nomadProviderAllocID) 240 241 // List all registrations using the service name and check the return 242 // object is as expected. There are some details we cannot assert, such as 243 // node ID and address. 244 expectedNomadService := api.ServiceRegistration{ 245 ServiceName: serviceName, 246 Namespace: api.DefaultNamespace, 247 Datacenter: "dc1", 248 JobID: jobID, 249 AllocID: nomadProviderAllocID, 250 Tags: []string{"foo", "bar"}, 251 } 252 requireEventuallyNomadService(t, &expectedNomadService, "") 253 } 254 nomadServiceTestFn() 255 256 // Register the "modified" job which changes the service provider from 257 // Nomad to Consul. Updating the provider should be an in-place update to 258 // the allocation. 259 allocStubs := e2eutil.RegisterAndWaitForAllocs(t, nomadClient, jobConsulProvider, jobID, "") 260 require.Len(t, allocStubs, 1) 261 require.Equal(t, nomadProviderAllocID, allocStubs[0].ID) 262 263 // We should now have zero service registrations for the given serviceName. 264 require.Eventually(t, func() bool { 265 services, _, err := nomadClient.Services().Get(serviceName, nil) 266 if err != nil { 267 return false 268 } 269 return len(services) == 0 270 }, defaultWaitForTime, defaultTickTime) 271 272 // Grab the Consul client for use. 273 consulClient := e2eutil.ConsulClient(t) 274 275 // List all registrations using the service name and check the return 276 // object is as expected. There are some details we cannot assert. 277 require.Eventually(t, func() bool { 278 consulServices, _, err := consulClient.Catalog().Service(serviceName, "", nil) 279 if err != nil { 280 return false 281 } 282 283 // Perform the checks. 284 if len(consulServices) != 1 { 285 return false 286 } 287 if consulServices[0].ServiceName != "http-api" { 288 return false 289 } 290 if !strings.Contains(consulServices[0].ServiceID, nomadProviderAllocID) { 291 return false 292 } 293 if !reflect.DeepEqual(consulServices[0].ServiceTags, []string{"foo", "bar"}) { 294 return false 295 } 296 return reflect.DeepEqual(consulServices[0].ServiceMeta, map[string]string{"external-source": "nomad"}) 297 }, defaultWaitForTime, defaultTickTime) 298 299 // Rerun the Nomad test function. This will register the service back with 300 // the Nomad provider and make sure it is found as expected. 301 nomadServiceTestFn() 302 303 // Ensure the service has now been removed from Consul. Wrap this in an 304 // eventual as Consul updates are a-sync. 305 require.Eventually(t, func() bool { 306 consulServices, _, err := consulClient.Catalog().Service(serviceName, "", nil) 307 if err != nil { 308 return false 309 } 310 return len(consulServices) == 0 311 }, defaultWaitForTime, defaultTickTime) 312 313 // Purge the job and ensure the service is removed. If this completes 314 // successfully, cancel the deferred cleanup. 315 e2eutil.CleanupJobsAndGC(t, &jobIDs)() 316 cancel() 317 318 require.Eventually(t, func() bool { 319 services, _, err := nomadClient.Services().Get(serviceName, nil) 320 if err != nil { 321 return false 322 } 323 return len(services) == 0 324 }, defaultWaitForTime, defaultTickTime) 325 } 326 327 // requireEventuallyNomadService is a helper which performs an eventual check 328 // against Nomad for a single service. Test cases which expect more than a 329 // single response should implement their own assertion, to handle ordering 330 // problems. 331 func requireEventuallyNomadService(t *testing.T, expected *api.ServiceRegistration, filter string) { 332 opts := (*api.QueryOptions)(nil) 333 if filter != "" { 334 opts = &api.QueryOptions{ 335 Filter: filter, 336 } 337 } 338 339 require.Eventually(t, func() bool { 340 services, _, err := e2eutil.NomadClient(t).Services().Get(expected.ServiceName, opts) 341 if err != nil { 342 return false 343 } 344 345 if len(services) != 1 { 346 return false 347 } 348 349 // ensure each matching service meets expectations 350 if services[0].ServiceName != expected.ServiceName { 351 return false 352 } 353 if services[0].Namespace != api.DefaultNamespace { 354 return false 355 } 356 if services[0].Datacenter != "dc1" { 357 return false 358 } 359 if services[0].JobID != expected.JobID { 360 return false 361 } 362 if services[0].AllocID != expected.AllocID { 363 return false 364 } 365 if !slices.Equal(services[0].Tags, expected.Tags) { 366 return false 367 } 368 369 return true 370 371 }, defaultWaitForTime, defaultTickTime) 372 }