github.com/ilhicas/nomad@v0.11.8/command/agent/consul/int_test.go (about)

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