github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/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 "io/ioutil" 11 "net" 12 "net/http" 13 "net/url" 14 "os" 15 "path/filepath" 16 "strings" 17 "time" 18 19 "github.com/juju/clock/testclock" 20 "github.com/juju/pubsub" 21 "github.com/juju/testing" 22 jc "github.com/juju/testing/checkers" 23 gc "gopkg.in/check.v1" 24 "gopkg.in/juju/worker.v1/workertest" 25 26 "github.com/juju/juju/api" 27 "github.com/juju/juju/apiserver/apiserverhttp" 28 "github.com/juju/juju/pubsub/apiserver" 29 coretesting "github.com/juju/juju/testing" 30 "github.com/juju/juju/worker/httpserver" 31 ) 32 33 type workerFixture struct { 34 testing.IsolationSuite 35 prometheusRegisterer stubPrometheusRegisterer 36 agentName string 37 mux *apiserverhttp.Mux 38 clock *testclock.Clock 39 hub *pubsub.StructuredHub 40 config httpserver.Config 41 logDir string 42 stub testing.Stub 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 } 71 } 72 73 type WorkerValidationSuite struct { 74 workerFixture 75 } 76 77 var _ = gc.Suite(&WorkerValidationSuite{}) 78 79 func (s *WorkerValidationSuite) TestValidateErrors(c *gc.C) { 80 type test struct { 81 f func(*httpserver.Config) 82 expect string 83 } 84 tests := []test{{ 85 func(cfg *httpserver.Config) { cfg.AgentName = "" }, 86 "empty AgentName not valid", 87 }, { 88 func(cfg *httpserver.Config) { cfg.TLSConfig = nil }, 89 "nil TLSConfig not valid", 90 }, { 91 func(cfg *httpserver.Config) { cfg.Mux = nil }, 92 "nil Mux not valid", 93 }, { 94 func(cfg *httpserver.Config) { cfg.PrometheusRegisterer = nil }, 95 "nil PrometheusRegisterer not valid", 96 }} 97 for i, test := range tests { 98 c.Logf("test #%d (%s)", i, test.expect) 99 s.testValidateError(c, test.f, test.expect) 100 } 101 } 102 103 func (s *WorkerValidationSuite) testValidateError(c *gc.C, f func(*httpserver.Config), expect string) { 104 config := s.config 105 f(&config) 106 w, err := httpserver.NewWorker(config) 107 if !c.Check(err, gc.NotNil) { 108 workertest.DirtyKill(c, w) 109 return 110 } 111 c.Check(w, gc.IsNil) 112 c.Check(err, gc.ErrorMatches, expect) 113 } 114 115 type WorkerSuite struct { 116 workerFixture 117 worker *httpserver.Worker 118 } 119 120 var _ = gc.Suite(&WorkerSuite{}) 121 122 func (s *WorkerSuite) SetUpTest(c *gc.C) { 123 s.workerFixture.SetUpTest(c) 124 worker, err := httpserver.NewWorker(s.config) 125 c.Assert(err, jc.ErrorIsNil) 126 s.AddCleanup(func(c *gc.C) { 127 workertest.DirtyKill(c, worker) 128 }) 129 s.worker = worker 130 } 131 132 func (s *WorkerSuite) TestStartStop(c *gc.C) { 133 workertest.CleanKill(c, s.worker) 134 } 135 136 func (s *WorkerSuite) TestURL(c *gc.C) { 137 url := s.worker.URL() 138 c.Assert(url, gc.Matches, "https://.*") 139 } 140 141 func (s *WorkerSuite) TestURLWorkerDead(c *gc.C) { 142 workertest.CleanKill(c, s.worker) 143 url := s.worker.URL() 144 c.Assert(url, gc.Matches, "") 145 } 146 147 func (s *WorkerSuite) TestRoundTrip(c *gc.C) { 148 s.makeRequest(c, s.worker.URL()) 149 } 150 151 func (s *WorkerSuite) makeRequest(c *gc.C, url string) { 152 s.mux.AddHandler("GET", "/hello/:name", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 153 w.WriteHeader(http.StatusOK) 154 io.WriteString(w, "hello, "+r.URL.Query().Get(":name")) 155 })) 156 client := &http.Client{ 157 Transport: &http.Transport{ 158 TLSClientConfig: s.config.TLSConfig, 159 }, 160 } 161 resp, err := client.Get(url + "/hello/world") 162 c.Assert(err, jc.ErrorIsNil) 163 defer resp.Body.Close() 164 165 c.Assert(resp.StatusCode, gc.Equals, http.StatusOK) 166 out, err := ioutil.ReadAll(resp.Body) 167 c.Assert(err, jc.ErrorIsNil) 168 c.Assert(string(out), gc.Equals, "hello, world") 169 } 170 171 func (s *WorkerSuite) TestWaitsForClients(c *gc.C) { 172 // Check that the httpserver stays functional until any clients 173 // have finished with it. 174 s.mux.AddClient() 175 176 // Shouldn't take effect until the client has done. 177 s.worker.Kill() 178 179 waitResult := make(chan error) 180 go func() { 181 waitResult <- s.worker.Wait() 182 }() 183 184 select { 185 case <-waitResult: 186 c.Fatalf("didn't wait for clients to finish with the mux") 187 case <-time.After(coretesting.ShortWait): 188 } 189 190 s.mux.ClientDone() 191 select { 192 case err := <-waitResult: 193 c.Assert(err, jc.ErrorIsNil) 194 case <-time.After(coretesting.LongWait): 195 c.Fatalf("didn't stop after clients were finished") 196 } 197 // Normal exit, no debug file. 198 _, err := os.Stat(filepath.Join(s.logDir, "apiserver-debug.log")) 199 c.Assert(err, jc.Satisfies, os.IsNotExist) 200 } 201 202 func (s *WorkerSuite) TestExitsWithTardyClients(c *gc.C) { 203 // Check that the httpserver shuts down eventually if 204 // clients appear to be stuck. 205 s.mux.AddClient() 206 207 // Shouldn't take effect until the timeout. 208 s.worker.Kill() 209 210 waitResult := make(chan error) 211 go func() { 212 waitResult <- s.worker.Wait() 213 }() 214 215 select { 216 case <-waitResult: 217 c.Fatalf("didn't wait for timeout") 218 case <-time.After(coretesting.ShortWait): 219 } 220 221 // Don't call s.mux.ClientDone(), timeout instead. 222 s.clock.Advance(1 * time.Minute) 223 select { 224 case err := <-waitResult: 225 c.Assert(err, jc.ErrorIsNil) 226 case <-time.After(coretesting.LongWait): 227 c.Fatalf("didn't stop after timeout") 228 } 229 // There should be a log file with goroutines. 230 data, err := ioutil.ReadFile(filepath.Join(s.logDir, "apiserver-debug.log")) 231 c.Assert(err, jc.ErrorIsNil) 232 lines := strings.Split(string(data), "\n") 233 c.Assert(len(lines), jc.GreaterThan, 1) 234 c.Assert(lines[1], gc.Matches, "goroutine profile:.*") 235 } 236 237 func (s *WorkerSuite) TestMinTLSVersion(c *gc.C) { 238 parsed, err := url.Parse(s.worker.URL()) 239 c.Assert(err, jc.ErrorIsNil) 240 241 tlsConfig := s.config.TLSConfig 242 // Specify an unsupported TLS version 243 tlsConfig.MaxVersion = tls.VersionSSL30 244 245 conn, err := tls.Dial("tcp", parsed.Host, tlsConfig) 246 c.Assert(err, gc.ErrorMatches, ".*protocol version not supported") 247 c.Assert(conn, gc.IsNil) 248 } 249 250 func (s *WorkerSuite) TestHeldListener(c *gc.C) { 251 // Worker url comes back as "" when the worker is dying. 252 url := s.worker.URL() 253 254 // Simulate having a slow request being handled. 255 s.mux.AddClient() 256 257 err := s.mux.AddHandler("GET", "/quick", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 258 w.WriteHeader(http.StatusOK) 259 })) 260 c.Assert(err, jc.ErrorIsNil) 261 262 quickErr := make(chan error) 263 request := func() { 264 // Make a new client each request so we don't reuse 265 // connections. 266 client := &http.Client{ 267 Transport: &http.Transport{ 268 TLSClientConfig: s.config.TLSConfig, 269 }, 270 } 271 _, err := client.Get(url + "/quick") 272 quickErr <- err 273 } 274 275 // Sanity check - the quick one should be quick normally. 276 go request() 277 278 select { 279 case err := <-quickErr: 280 c.Assert(err, jc.ErrorIsNil) 281 case <-time.After(coretesting.ShortWait): 282 c.Fatalf("timed out waiting for quick request") 283 } 284 285 // Stop the server. 286 s.worker.Kill() 287 288 // Eventually quick requests get blocked by the held listener. 289 var quickBlocked bool 290 attempts: 291 for a := coretesting.LongAttempt.Start(); a.Next(); { 292 go request() 293 select { 294 case <-quickErr: 295 case <-time.After(coretesting.ShortWait): 296 quickBlocked = true 297 break attempts 298 } 299 } 300 c.Assert(quickBlocked, gc.Equals, true) 301 302 // The server doesn't die yet - it's kept alive by the slow 303 // request. 304 workertest.CheckAlive(c, s.worker) 305 306 // Let the slow request complete. See that the server 307 // stops, and the 2nd request completes. 308 s.mux.ClientDone() 309 310 select { 311 case err := <-quickErr: 312 // It doesn't really matter what the error is. 313 c.Assert(err, gc.NotNil) 314 case <-time.After(coretesting.ShortWait): 315 c.Fatalf("timed out waiting for 2nd quick request") 316 } 317 workertest.CheckKilled(c, s.worker) 318 } 319 320 type WorkerControllerPortSuite struct { 321 workerFixture 322 } 323 324 var _ = gc.Suite(&WorkerControllerPortSuite{}) 325 326 func (s *WorkerControllerPortSuite) newWorker(c *gc.C) *httpserver.Worker { 327 worker, err := httpserver.NewWorker(s.config) 328 c.Assert(err, jc.ErrorIsNil) 329 s.AddCleanup(func(c *gc.C) { 330 workertest.DirtyKill(c, worker) 331 }) 332 return worker 333 } 334 335 func (s *WorkerControllerPortSuite) TestDualPortListenerWithDelay(c *gc.C) { 336 err := s.mux.AddHandler("GET", "/quick", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 337 w.WriteHeader(http.StatusOK) 338 })) 339 c.Assert(err, jc.ErrorIsNil) 340 341 request := func(url string) error { 342 client := &http.Client{ 343 Transport: &http.Transport{ 344 TLSClientConfig: s.config.TLSConfig, 345 }, 346 } 347 _, err := client.Get(url + "/quick") 348 return err 349 } 350 351 // Make a worker with a controller API port. 352 port := testing.FindTCPPort() 353 controllerPort := testing.FindTCPPort() 354 s.config.APIPort = port 355 s.config.ControllerAPIPort = controllerPort 356 s.config.APIPortOpenDelay = 10 * time.Second 357 358 worker := s.newWorker(c) 359 360 // The worker reports its URL as the controller port. 361 controllerURL := worker.URL() 362 parsed, err := url.Parse(controllerURL) 363 c.Assert(err, jc.ErrorIsNil) 364 c.Assert(parsed.Port(), gc.Equals, fmt.Sprint(controllerPort)) 365 366 reportPorts := map[string]interface{}{ 367 "controller": fmt.Sprintf("[::]:%d", s.config.ControllerAPIPort), 368 "status": "waiting for signal to open agent port", 369 } 370 report := map[string]interface{}{ 371 "api-port": s.config.APIPort, 372 "api-port-open-delay": s.config.APIPortOpenDelay, 373 "controller-api-port": s.config.ControllerAPIPort, 374 "status": "running", 375 "ports": reportPorts, 376 } 377 c.Check(worker.Report(), jc.DeepEquals, report) 378 379 // Requests on that port work. 380 c.Assert(request(controllerURL), jc.ErrorIsNil) 381 382 // Requests on the regular API port fail to connect. 383 parsed.Host = net.JoinHostPort(parsed.Hostname(), fmt.Sprint(port)) 384 normalURL := parsed.String() 385 c.Assert(request(normalURL), gc.ErrorMatches, `.*: connection refused$`) 386 387 // Getting a connection from someone else doesn't unblock. 388 handled, err := s.hub.Publish(apiserver.ConnectTopic, apiserver.APIConnection{ 389 AgentTag: "machine-13", 390 Origin: s.agentName, 391 }) 392 c.Assert(err, jc.ErrorIsNil) 393 select { 394 case <-handled: 395 case <-time.After(testing.LongWait): 396 c.Fatalf("the handler should have exited early and not be waiting") 397 } 398 399 // Send API details on the hub - still no luck connecting on the 400 // non-controller port. 401 _, err = s.hub.Publish(apiserver.ConnectTopic, apiserver.APIConnection{ 402 AgentTag: s.agentName, 403 Origin: s.agentName, 404 }) 405 c.Assert(err, jc.ErrorIsNil) 406 407 err = s.clock.WaitAdvance(5*time.Second, coretesting.LongWait, 1) 408 c.Assert(err, jc.ErrorIsNil) 409 c.Assert(request(controllerURL), jc.ErrorIsNil) 410 c.Assert(request(normalURL), gc.ErrorMatches, `.*: connection refused$`) 411 412 reportPorts["status"] = "waiting prior to opening agent port" 413 c.Check(worker.Report(), jc.DeepEquals, report) 414 415 // After the required delay the port eventually opens. 416 err = s.clock.WaitAdvance(5*time.Second, coretesting.LongWait, 1) 417 418 // The reported url changes to the regular port. 419 for a := coretesting.LongAttempt.Start(); a.Next(); { 420 if worker.URL() == normalURL { 421 break 422 } 423 } 424 c.Assert(worker.URL(), gc.Equals, normalURL) 425 426 // Requests on both ports work. 427 c.Assert(request(controllerURL), jc.ErrorIsNil) 428 c.Assert(request(normalURL), jc.ErrorIsNil) 429 430 delete(reportPorts, "status") 431 reportPorts["agent"] = fmt.Sprintf("[::]:%d", s.config.APIPort) 432 c.Check(worker.Report(), jc.DeepEquals, report) 433 }