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 }