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  }