github.com/hernad/nomad@v1.6.112/command/agent/consul/int_test.go (about)

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