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  }