github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/command/agent/consul/int_test.go (about)

     1  package consul_test
     2  
     3  import (
     4  	"context"
     5  	"io/ioutil"
     6  	"testing"
     7  	"time"
     8  
     9  	consulapi "github.com/hashicorp/consul/api"
    10  	"github.com/hashicorp/consul/sdk/testutil"
    11  	log "github.com/hashicorp/go-hclog"
    12  	"github.com/hashicorp/nomad/ci"
    13  	"github.com/hashicorp/nomad/client/allocdir"
    14  	"github.com/hashicorp/nomad/client/allocrunner/taskrunner"
    15  	"github.com/hashicorp/nomad/client/config"
    16  	"github.com/hashicorp/nomad/client/devicemanager"
    17  	"github.com/hashicorp/nomad/client/pluginmanager/drivermanager"
    18  	regMock "github.com/hashicorp/nomad/client/serviceregistration/mock"
    19  	"github.com/hashicorp/nomad/client/serviceregistration/wrapper"
    20  	"github.com/hashicorp/nomad/client/state"
    21  	"github.com/hashicorp/nomad/client/vaultclient"
    22  	"github.com/hashicorp/nomad/command/agent/consul"
    23  	"github.com/hashicorp/nomad/helper/testlog"
    24  	"github.com/hashicorp/nomad/nomad/mock"
    25  	"github.com/hashicorp/nomad/nomad/structs"
    26  	"github.com/stretchr/testify/require"
    27  )
    28  
    29  type mockUpdater struct {
    30  	logger log.Logger
    31  }
    32  
    33  func (m *mockUpdater) TaskStateUpdated() {
    34  	m.logger.Named("mock.updater").Debug("Update!")
    35  }
    36  
    37  // TestConsul_Integration asserts TaskRunner properly registers and deregisters
    38  // services and checks with Consul using an embedded Consul agent.
    39  func TestConsul_Integration(t *testing.T) {
    40  	ci.Parallel(t)
    41  
    42  	if testing.Short() {
    43  		t.Skip("-short set; skipping")
    44  	}
    45  	r := require.New(t)
    46  
    47  	// Create an embedded Consul server
    48  	testconsul, err := testutil.NewTestServerConfigT(t, func(c *testutil.TestServerConfig) {
    49  		c.Peering = nil  // fix for older versions of Consul (<1.13.0) that don't support peering
    50  		// If -v wasn't specified squelch consul logging
    51  		if !testing.Verbose() {
    52  			c.Stdout = ioutil.Discard
    53  			c.Stderr = ioutil.Discard
    54  		}
    55  	})
    56  	if err != nil {
    57  		t.Fatalf("error starting test consul server: %v", err)
    58  	}
    59  	defer testconsul.Stop()
    60  
    61  	conf := config.DefaultConfig()
    62  	conf.Node = mock.Node()
    63  	conf.ConsulConfig.Addr = testconsul.HTTPAddr
    64  	consulConfig, err := conf.ConsulConfig.ApiConfig()
    65  	if err != nil {
    66  		t.Fatalf("error generating consul config: %v", err)
    67  	}
    68  
    69  	conf.StateDir = t.TempDir()
    70  	conf.AllocDir = t.TempDir()
    71  
    72  	alloc := mock.Alloc()
    73  	task := alloc.Job.TaskGroups[0].Tasks[0]
    74  	task.Driver = "mock_driver"
    75  	task.Config = map[string]interface{}{
    76  		"run_for": "1h",
    77  	}
    78  
    79  	// Choose a port that shouldn't be in use
    80  	netResource := &structs.NetworkResource{
    81  		Device:        "eth0",
    82  		IP:            "127.0.0.1",
    83  		MBits:         50,
    84  		ReservedPorts: []structs.Port{{Label: "http", Value: 3}},
    85  	}
    86  	alloc.AllocatedResources.Tasks["web"].Networks[0] = netResource
    87  
    88  	task.Services = []*structs.Service{
    89  		{
    90  			Name:      "httpd",
    91  			PortLabel: "http",
    92  			Tags:      []string{"nomad", "test", "http"},
    93  			Provider:  structs.ServiceProviderConsul,
    94  			Checks: []*structs.ServiceCheck{
    95  				{
    96  					Name:     "httpd-http-check",
    97  					Type:     "http",
    98  					Path:     "/",
    99  					Protocol: "http",
   100  					Interval: 9000 * time.Hour,
   101  					Timeout:  1, // fail as fast as possible
   102  				},
   103  				{
   104  					Name:     "httpd-script-check",
   105  					Type:     "script",
   106  					Command:  "/bin/true",
   107  					Interval: 10 * time.Second,
   108  					Timeout:  10 * time.Second,
   109  				},
   110  			},
   111  		},
   112  		{
   113  			Name:      "httpd2",
   114  			PortLabel: "http",
   115  			Provider:  structs.ServiceProviderConsul,
   116  			Tags: []string{
   117  				"test",
   118  				// Use URL-unfriendly tags to test #3620
   119  				"public-test.ettaviation.com:80/ redirect=302,https://test.ettaviation.com",
   120  				"public-test.ettaviation.com:443/",
   121  			},
   122  		},
   123  	}
   124  
   125  	logger := testlog.HCLogger(t)
   126  	logUpdate := &mockUpdater{logger}
   127  	allocDir := allocdir.NewAllocDir(logger, conf.AllocDir, alloc.ID)
   128  	if err := allocDir.Build(); err != nil {
   129  		t.Fatalf("error building alloc dir: %v", err)
   130  	}
   131  	t.Cleanup(func() {
   132  		r.NoError(allocDir.Destroy())
   133  	})
   134  	taskDir := allocDir.NewTaskDir(task.Name)
   135  	vclient := vaultclient.NewMockVaultClient()
   136  	consulClient, err := consulapi.NewClient(consulConfig)
   137  	r.Nil(err)
   138  
   139  	namespacesClient := consul.NewNamespacesClient(consulClient.Namespaces(), consulClient.Agent())
   140  	serviceClient := consul.NewServiceClient(consulClient.Agent(), namespacesClient, testlog.HCLogger(t), true)
   141  	defer serviceClient.Shutdown() // just-in-case cleanup
   142  	consulRan := make(chan struct{})
   143  	go func() {
   144  		serviceClient.Run()
   145  		close(consulRan)
   146  	}()
   147  
   148  	// Create a closed channel to mock TaskCoordinator.startConditionForTask.
   149  	// Closed channel indicates this task is not blocked on prestart hooks.
   150  	closedCh := make(chan struct{})
   151  	close(closedCh)
   152  
   153  	// Build the config
   154  	config := &taskrunner.Config{
   155  		Alloc:               alloc,
   156  		ClientConfig:        conf,
   157  		Consul:              serviceClient,
   158  		Task:                task,
   159  		TaskDir:             taskDir,
   160  		Logger:              logger,
   161  		Vault:               vclient,
   162  		StateDB:             state.NoopDB{},
   163  		StateUpdater:        logUpdate,
   164  		DeviceManager:       devicemanager.NoopMockManager(),
   165  		DriverManager:       drivermanager.TestDriverManager(t),
   166  		StartConditionMetCh: closedCh,
   167  		ServiceRegWrapper:   wrapper.NewHandlerWrapper(logger, serviceClient, regMock.NewServiceRegistrationHandler(logger)),
   168  	}
   169  
   170  	tr, err := taskrunner.NewTaskRunner(config)
   171  	r.NoError(err)
   172  	go tr.Run()
   173  	defer func() {
   174  		// Make sure we always shutdown task runner when the test exits
   175  		select {
   176  		case <-tr.WaitCh():
   177  			// Exited cleanly, no need to kill
   178  		default:
   179  			tr.Kill(context.Background(), &structs.TaskEvent{}) // just in case
   180  		}
   181  	}()
   182  
   183  	// Block waiting for the service to appear
   184  	catalog := consulClient.Catalog()
   185  	res, meta, err := catalog.Service("httpd2", "test", nil)
   186  	r.Nil(err)
   187  
   188  	for i := 0; len(res) == 0 && i < 10; i++ {
   189  		//Expected initial request to fail, do a blocking query
   190  		res, meta, err = catalog.Service("httpd2", "test", &consulapi.QueryOptions{WaitIndex: meta.LastIndex + 1, WaitTime: 3 * time.Second})
   191  		if err != nil {
   192  			t.Fatalf("error querying for service: %v", err)
   193  		}
   194  	}
   195  	r.Len(res, 1)
   196  
   197  	// Truncate results
   198  	res = res[:]
   199  
   200  	// Assert the service with the checks exists
   201  	for i := 0; len(res) == 0 && i < 10; i++ {
   202  		res, meta, err = catalog.Service("httpd", "http", &consulapi.QueryOptions{WaitIndex: meta.LastIndex + 1, WaitTime: 3 * time.Second})
   203  		r.Nil(err)
   204  	}
   205  	r.Len(res, 1)
   206  
   207  	// Assert the script check passes (mock_driver script checks always
   208  	// pass) after having time to run once
   209  	time.Sleep(2 * time.Second)
   210  	checks, _, err := consulClient.Health().Checks("httpd", nil)
   211  	r.Nil(err)
   212  	r.Len(checks, 2)
   213  
   214  	for _, check := range checks {
   215  		if expected := "httpd"; check.ServiceName != expected {
   216  			t.Fatalf("expected checks to be for %q but found service name = %q", expected, check.ServiceName)
   217  		}
   218  		switch check.Name {
   219  		case "httpd-http-check":
   220  			// Port check should fail
   221  			if expected := consulapi.HealthCritical; check.Status != expected {
   222  				t.Errorf("expected %q status to be %q but found %q", check.Name, expected, check.Status)
   223  			}
   224  		case "httpd-script-check":
   225  			// mock_driver script checks always succeed
   226  			if expected := consulapi.HealthPassing; check.Status != expected {
   227  				t.Errorf("expected %q status to be %q but found %q", check.Name, expected, check.Status)
   228  			}
   229  		default:
   230  			t.Errorf("unexpected check %q with status %q", check.Name, check.Status)
   231  		}
   232  	}
   233  
   234  	// Assert the service client returns all the checks for the allocation.
   235  	reg, err := serviceClient.AllocRegistrations(alloc.ID)
   236  	if err != nil {
   237  		t.Fatalf("unexpected error retrieving allocation checks: %v", err)
   238  	}
   239  	if reg == nil {
   240  		t.Fatalf("Unexpected nil allocation registration")
   241  	}
   242  	if snum := reg.NumServices(); snum != 2 {
   243  		t.Fatalf("Unexpected number of services registered. Got %d; want 2", snum)
   244  	}
   245  	if cnum := reg.NumChecks(); cnum != 2 {
   246  		t.Fatalf("Unexpected number of checks registered. Got %d; want 2", cnum)
   247  	}
   248  
   249  	logger.Debug("killing task")
   250  
   251  	// Kill the task
   252  	tr.Kill(context.Background(), &structs.TaskEvent{})
   253  
   254  	select {
   255  	case <-tr.WaitCh():
   256  	case <-time.After(10 * time.Second):
   257  		t.Fatalf("timed out waiting for Run() to exit")
   258  	}
   259  
   260  	// Shutdown Consul ServiceClient to ensure all pending operations complete
   261  	if err := serviceClient.Shutdown(); err != nil {
   262  		t.Errorf("error shutting down Consul ServiceClient: %v", err)
   263  	}
   264  
   265  	// Ensure Consul is clean
   266  	services, _, err := catalog.Services(nil)
   267  	r.Nil(err)
   268  	r.Len(services, 1)
   269  	r.Contains(services, "consul")
   270  }