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