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  }