github.com/juju/juju@v0.0.0-20240327075706-a90865de2538/worker/httpserver/worker_test.go (about) 1 // Copyright 2018 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package httpserver_test 5 6 import ( 7 "crypto/tls" 8 "fmt" 9 "io" 10 "net" 11 "net/http" 12 "net/url" 13 "os" 14 "path/filepath" 15 "strings" 16 "time" 17 18 "github.com/juju/clock/testclock" 19 "github.com/juju/loggo" 20 mgotesting "github.com/juju/mgo/v3/testing" 21 "github.com/juju/pubsub/v2" 22 "github.com/juju/testing" 23 jc "github.com/juju/testing/checkers" 24 "github.com/juju/worker/v3/workertest" 25 gc "gopkg.in/check.v1" 26 27 "github.com/juju/juju/api" 28 "github.com/juju/juju/apiserver/apiserverhttp" 29 "github.com/juju/juju/pubsub/apiserver" 30 coretesting "github.com/juju/juju/testing" 31 "github.com/juju/juju/worker/httpserver" 32 ) 33 34 type workerFixture struct { 35 testing.IsolationSuite 36 prometheusRegisterer stubPrometheusRegisterer 37 agentName string 38 mux *apiserverhttp.Mux 39 clock *testclock.Clock 40 hub *pubsub.StructuredHub 41 config httpserver.Config 42 logDir string 43 } 44 45 func (s *workerFixture) SetUpTest(c *gc.C) { 46 s.IsolationSuite.SetUpTest(c) 47 certPool, err := api.CreateCertPool(coretesting.CACert) 48 c.Assert(err, jc.ErrorIsNil) 49 tlsConfig := api.NewTLSConfig(certPool) 50 tlsConfig.ServerName = "juju-apiserver" 51 tlsConfig.Certificates = []tls.Certificate{*coretesting.ServerTLSCert} 52 s.prometheusRegisterer = stubPrometheusRegisterer{} 53 s.mux = apiserverhttp.NewMux() 54 s.clock = testclock.NewClock(time.Now()) 55 s.hub = pubsub.NewStructuredHub(nil) 56 s.agentName = "machine-42" 57 s.logDir = c.MkDir() 58 s.config = httpserver.Config{ 59 AgentName: s.agentName, 60 Clock: s.clock, 61 TLSConfig: tlsConfig, 62 Mux: s.mux, 63 PrometheusRegisterer: &s.prometheusRegisterer, 64 LogDir: s.logDir, 65 MuxShutdownWait: 1 * time.Minute, 66 Hub: s.hub, 67 APIPort: 0, 68 APIPortOpenDelay: 0, 69 ControllerAPIPort: 0, 70 Logger: loggo.GetLogger("test"), 71 } 72 } 73 74 type WorkerValidationSuite struct { 75 workerFixture 76 } 77 78 var _ = gc.Suite(&WorkerValidationSuite{}) 79 80 func (s *WorkerValidationSuite) TestValidateErrors(c *gc.C) { 81 type test struct { 82 f func(*httpserver.Config) 83 expect string 84 } 85 tests := []test{{ 86 func(cfg *httpserver.Config) { cfg.AgentName = "" }, 87 "empty AgentName not valid", 88 }, { 89 func(cfg *httpserver.Config) { cfg.TLSConfig = nil }, 90 "nil TLSConfig not valid", 91 }, { 92 func(cfg *httpserver.Config) { cfg.Mux = nil }, 93 "nil Mux not valid", 94 }, { 95 func(cfg *httpserver.Config) { cfg.PrometheusRegisterer = nil }, 96 "nil PrometheusRegisterer not valid", 97 }} 98 for i, test := range tests { 99 c.Logf("test #%d (%s)", i, test.expect) 100 s.testValidateError(c, test.f, test.expect) 101 } 102 } 103 104 func (s *WorkerValidationSuite) testValidateError(c *gc.C, f func(*httpserver.Config), expect string) { 105 config := s.config 106 f(&config) 107 w, err := httpserver.NewWorker(config) 108 if !c.Check(err, gc.NotNil) { 109 workertest.DirtyKill(c, w) 110 return 111 } 112 c.Check(w, gc.IsNil) 113 c.Check(err, gc.ErrorMatches, expect) 114 } 115 116 type WorkerSuite struct { 117 workerFixture 118 worker *httpserver.Worker 119 } 120 121 var _ = gc.Suite(&WorkerSuite{}) 122 123 func (s *WorkerSuite) SetUpTest(c *gc.C) { 124 s.workerFixture.SetUpTest(c) 125 worker, err := httpserver.NewWorker(s.config) 126 c.Assert(err, jc.ErrorIsNil) 127 s.AddCleanup(func(c *gc.C) { 128 workertest.DirtyKill(c, worker) 129 }) 130 s.worker = worker 131 } 132 133 func (s *WorkerSuite) TestStartStop(c *gc.C) { 134 workertest.CleanKill(c, s.worker) 135 } 136 137 func (s *WorkerSuite) TestURL(c *gc.C) { 138 url := s.worker.URL() 139 c.Assert(url, gc.Matches, "https://.*") 140 } 141 142 func (s *WorkerSuite) TestURLWorkerDead(c *gc.C) { 143 workertest.CleanKill(c, s.worker) 144 url := s.worker.URL() 145 c.Assert(url, gc.Matches, "") 146 } 147 148 func (s *WorkerSuite) TestRoundTrip(c *gc.C) { 149 s.makeRequest(c, s.worker.URL()) 150 } 151 152 func (s *WorkerSuite) makeRequest(c *gc.C, url string) { 153 s.mux.AddHandler("GET", "/hello/:name", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 154 w.WriteHeader(http.StatusOK) 155 io.WriteString(w, "hello, "+r.URL.Query().Get(":name")) 156 })) 157 client := &http.Client{ 158 Transport: &http.Transport{ 159 TLSClientConfig: s.config.TLSConfig, 160 }, 161 Timeout: testing.LongWait, 162 } 163 defer client.CloseIdleConnections() 164 resp, err := client.Get(url + "/hello/world") 165 c.Assert(err, jc.ErrorIsNil) 166 defer resp.Body.Close() 167 168 c.Assert(resp.StatusCode, gc.Equals, http.StatusOK) 169 out, err := io.ReadAll(resp.Body) 170 c.Assert(err, jc.ErrorIsNil) 171 c.Assert(string(out), gc.Equals, "hello, world") 172 } 173 174 func (s *WorkerSuite) TestWaitsForClients(c *gc.C) { 175 // Check that the httpserver stays functional until any clients 176 // have finished with it. 177 s.mux.AddClient() 178 179 // Shouldn't take effect until the client has done. 180 s.worker.Kill() 181 182 waitResult := make(chan error) 183 go func() { 184 waitResult <- s.worker.Wait() 185 }() 186 187 select { 188 case <-waitResult: 189 c.Fatalf("didn't wait for clients to finish with the mux") 190 case <-time.After(coretesting.ShortWait): 191 } 192 193 s.mux.ClientDone() 194 select { 195 case err := <-waitResult: 196 c.Assert(err, jc.ErrorIsNil) 197 case <-time.After(coretesting.LongWait): 198 c.Fatalf("didn't stop after clients were finished") 199 } 200 // Normal exit, no debug file. 201 _, err := os.Stat(filepath.Join(s.logDir, "apiserver-debug.log")) 202 c.Assert(err, jc.Satisfies, os.IsNotExist) 203 } 204 205 func (s *WorkerSuite) TestExitsWithTardyClients(c *gc.C) { 206 // Check that the httpserver shuts down eventually if 207 // clients appear to be stuck. 208 s.mux.AddClient() 209 210 // Shouldn't take effect until the timeout. 211 s.worker.Kill() 212 213 waitResult := make(chan error) 214 go func() { 215 waitResult <- s.worker.Wait() 216 }() 217 218 select { 219 case <-waitResult: 220 c.Fatalf("didn't wait for timeout") 221 case <-time.After(coretesting.ShortWait): 222 } 223 224 // Don't call s.mux.ClientDone(), timeout instead. 225 s.clock.Advance(1 * time.Minute) 226 select { 227 case err := <-waitResult: 228 c.Assert(err, jc.ErrorIsNil) 229 case <-time.After(coretesting.LongWait): 230 c.Fatalf("didn't stop after timeout") 231 } 232 // There should be a log file with goroutines. 233 data, err := os.ReadFile(filepath.Join(s.logDir, "apiserver-debug.log")) 234 c.Assert(err, jc.ErrorIsNil) 235 lines := strings.Split(string(data), "\n") 236 c.Assert(len(lines), jc.GreaterThan, 1) 237 c.Assert(lines[1], gc.Matches, "goroutine profile:.*") 238 } 239 240 func (s *WorkerSuite) TestMinTLSVersion(c *gc.C) { 241 parsed, err := url.Parse(s.worker.URL()) 242 c.Assert(err, jc.ErrorIsNil) 243 244 tlsConfig := s.config.TLSConfig 245 // Specify an unsupported TLS version 246 tlsConfig.MaxVersion = tls.VersionSSL30 247 248 conn, err := tls.Dial("tcp", parsed.Host, tlsConfig) 249 c.Assert(err, gc.ErrorMatches, ".*tls:.*version.*") 250 c.Assert(conn, gc.IsNil) 251 } 252 253 func (s *WorkerSuite) TestHeldListener(c *gc.C) { 254 // Worker url comes back as "" when the worker is dying. 255 url := s.worker.URL() 256 257 // Simulate having a slow request being handled. 258 s.mux.AddClient() 259 260 err := s.mux.AddHandler("GET", "/quick", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 261 w.WriteHeader(http.StatusOK) 262 })) 263 c.Assert(err, jc.ErrorIsNil) 264 265 quickErr := make(chan error) 266 request := func() { 267 // Make a new client each request so we don't reuse 268 // connections. 269 client := &http.Client{ 270 Transport: &http.Transport{ 271 TLSClientConfig: s.config.TLSConfig, 272 }, 273 Timeout: testing.LongWait, 274 } 275 defer client.CloseIdleConnections() 276 _, err := client.Get(url + "/quick") 277 quickErr <- err 278 } 279 280 // Sanity check - the quick one should be quick normally. 281 go request() 282 283 select { 284 case err := <-quickErr: 285 c.Assert(err, jc.ErrorIsNil) 286 case <-time.After(coretesting.LongWait): 287 c.Fatalf("timed out waiting for quick request") 288 } 289 290 // Stop the server. 291 s.worker.Kill() 292 293 // A very small sleep will allow the kill to be more likely to be processed 294 // by the running loop. 295 time.Sleep(10 * time.Millisecond) 296 297 // We actually try the quick request more than once, on the off chance 298 // that the worker hasn't finished processing the kill signal. Since we have 299 // no other way to check, we just try a quick request, and decide that if 300 // it doesn't respond quickly, the main loop is waithing for the clients to 301 // be done. 302 quickBlocked := false 303 timeout := time.After(coretesting.LongWait) 304 for !quickBlocked { 305 c.Log("try to hit the quick endpoint") 306 go request() 307 select { 308 case <-quickErr: 309 c.Log(" got a response") 310 case <-time.After(coretesting.ShortWait): 311 quickBlocked = true 312 case <-timeout: 313 c.Fatalf("worker not blocking") 314 } 315 } 316 317 // The server doesn't die yet - it's kept alive by the slow 318 // request. 319 workertest.CheckAlive(c, s.worker) 320 321 // Let the slow request complete. See that the server 322 // stops, and the 2nd request completes. 323 s.mux.ClientDone() 324 325 select { 326 case <-quickErr: 327 // There is a race in the queueing of the request. It is possible that 328 // the timer will fire a short wait before an unheld request gets to the 329 // phase where it would return nil. However this is only under significant 330 // load, and it isn't easy to synchronise. This is why we don't actually 331 // check the error. 332 // It doesn't really matter what the error is. 333 case <-time.After(coretesting.LongWait): 334 c.Fatalf("timed out waiting for 2nd quick request") 335 } 336 workertest.CheckKilled(c, s.worker) 337 } 338 339 type WorkerControllerPortSuite struct { 340 workerFixture 341 } 342 343 var _ = gc.Suite(&WorkerControllerPortSuite{}) 344 345 func (s *WorkerControllerPortSuite) newWorker(c *gc.C) *httpserver.Worker { 346 worker, err := httpserver.NewWorker(s.config) 347 c.Assert(err, jc.ErrorIsNil) 348 s.AddCleanup(func(c *gc.C) { 349 workertest.DirtyKill(c, worker) 350 }) 351 return worker 352 } 353 354 func (s *WorkerControllerPortSuite) TestDualPortListenerWithDelay(c *gc.C) { 355 err := s.mux.AddHandler("GET", "/quick", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 356 w.WriteHeader(http.StatusOK) 357 })) 358 c.Assert(err, jc.ErrorIsNil) 359 360 request := func(url string) error { 361 client := &http.Client{ 362 Transport: &http.Transport{ 363 TLSClientConfig: s.config.TLSConfig, 364 }, 365 Timeout: testing.LongWait, 366 } 367 defer client.CloseIdleConnections() 368 _, err := client.Get(url + "/quick") 369 return err 370 } 371 372 // Make a worker with a controller API port. 373 port := mgotesting.FindTCPPort() 374 controllerPort := mgotesting.FindTCPPort() 375 s.config.APIPort = port 376 s.config.ControllerAPIPort = controllerPort 377 s.config.APIPortOpenDelay = 10 * time.Second 378 379 worker := s.newWorker(c) 380 381 // The worker reports its URL as the controller port. 382 controllerURL := worker.URL() 383 parsed, err := url.Parse(controllerURL) 384 c.Assert(err, jc.ErrorIsNil) 385 c.Assert(parsed.Port(), gc.Equals, fmt.Sprint(controllerPort)) 386 387 reportPorts := map[string]interface{}{ 388 "controller": fmt.Sprintf("[::]:%d", s.config.ControllerAPIPort), 389 "status": "waiting for signal to open agent port", 390 } 391 report := map[string]interface{}{ 392 "api-port": s.config.APIPort, 393 "api-port-open-delay": s.config.APIPortOpenDelay, 394 "controller-api-port": s.config.ControllerAPIPort, 395 "status": "running", 396 "ports": reportPorts, 397 } 398 c.Check(worker.Report(), jc.DeepEquals, report) 399 400 // Requests on that port work. 401 c.Assert(request(controllerURL), jc.ErrorIsNil) 402 403 // Requests on the regular API port fail to connect. 404 parsed.Host = net.JoinHostPort(parsed.Hostname(), fmt.Sprint(port)) 405 normalURL := parsed.String() 406 c.Assert(request(normalURL), gc.ErrorMatches, `.*: connection refused$`) 407 408 // Getting a connection from someone else doesn't unblock. 409 handled, err := s.hub.Publish(apiserver.ConnectTopic, apiserver.APIConnection{ 410 AgentTag: "machine-13", 411 Origin: s.agentName, 412 }) 413 c.Assert(err, jc.ErrorIsNil) 414 select { 415 case <-pubsub.Wait(handled): 416 case <-time.After(testing.LongWait): 417 c.Fatalf("the handler should have exited early and not be waiting") 418 } 419 420 // Send API details on the hub - still no luck connecting on the 421 // non-controller port. 422 _, err = s.hub.Publish(apiserver.ConnectTopic, apiserver.APIConnection{ 423 AgentTag: s.agentName, 424 Origin: s.agentName, 425 }) 426 c.Assert(err, jc.ErrorIsNil) 427 428 err = s.clock.WaitAdvance(5*time.Second, coretesting.LongWait, 1) 429 c.Assert(err, jc.ErrorIsNil) 430 c.Assert(request(controllerURL), jc.ErrorIsNil) 431 c.Assert(request(normalURL), gc.ErrorMatches, `.*: connection refused$`) 432 433 reportPorts["status"] = "waiting prior to opening agent port" 434 c.Check(worker.Report(), jc.DeepEquals, report) 435 436 // After the required delay the port eventually opens. 437 err = s.clock.WaitAdvance(5*time.Second, coretesting.LongWait, 1) 438 c.Assert(err, jc.ErrorIsNil) 439 440 // The reported url changes to the regular port. 441 for a := coretesting.LongAttempt.Start(); a.Next(); { 442 if worker.URL() == normalURL { 443 break 444 } 445 } 446 c.Assert(worker.URL(), gc.Equals, normalURL) 447 448 // Requests on both ports work. 449 c.Assert(request(controllerURL), jc.ErrorIsNil) 450 c.Assert(request(normalURL), jc.ErrorIsNil) 451 452 delete(reportPorts, "status") 453 reportPorts["agent"] = fmt.Sprintf("[::]:%d", s.config.APIPort) 454 c.Check(worker.Report(), jc.DeepEquals, report) 455 } 456 457 func (s *WorkerControllerPortSuite) TestDualPortListenerWithDelayShutdown(c *gc.C) { 458 err := s.mux.AddHandler("GET", "/quick", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 459 w.WriteHeader(http.StatusOK) 460 })) 461 c.Assert(err, jc.ErrorIsNil) 462 463 request := func(url string) error { 464 client := &http.Client{ 465 Transport: &http.Transport{ 466 TLSClientConfig: s.config.TLSConfig, 467 }, 468 Timeout: testing.LongWait, 469 } 470 defer client.CloseIdleConnections() 471 _, err := client.Get(url + "/quick") 472 return err 473 } 474 // Make a worker with a controller API port. 475 port := mgotesting.FindTCPPort() 476 controllerPort := mgotesting.FindTCPPort() 477 s.config.APIPort = port 478 s.config.ControllerAPIPort = controllerPort 479 s.config.APIPortOpenDelay = 10 * time.Second 480 481 worker := s.newWorker(c) 482 controllerURL := worker.URL() 483 parsed, err := url.Parse(controllerURL) 484 c.Assert(err, jc.ErrorIsNil) 485 c.Assert(parsed.Port(), gc.Equals, fmt.Sprint(controllerPort)) 486 // Requests to controllerURL are successful, but normal requests are denied 487 c.Assert(request(controllerURL), jc.ErrorIsNil) 488 parsed.Host = net.JoinHostPort(parsed.Hostname(), fmt.Sprint(port)) 489 normalURL := parsed.String() 490 c.Assert(request(normalURL), gc.ErrorMatches, `.*: connection refused$`) 491 // Send API details on the hub - still no luck connecting on the 492 // non-controller port. 493 _, err = s.hub.Publish(apiserver.ConnectTopic, apiserver.APIConnection{ 494 AgentTag: s.agentName, 495 Origin: s.agentName, 496 }) 497 c.Assert(err, jc.ErrorIsNil) 498 // We exit cleanly even if we never tick the clock forward 499 workertest.CleanKill(c, worker) 500 }