github.com/juju/juju@v0.0.0-20240327075706-a90865de2538/worker/introspection/worker_test.go (about)

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package introspection_test
     5  
     6  import (
     7  	"fmt"
     8  	"io"
     9  	"net"
    10  	"net/http"
    11  	"net/url"
    12  	"os"
    13  	"runtime"
    14  	"strings"
    15  	"time"
    16  
    17  	"github.com/juju/clock/testclock"
    18  	"github.com/juju/loggo"
    19  	"github.com/juju/pubsub/v2"
    20  	"github.com/juju/testing"
    21  	jc "github.com/juju/testing/checkers"
    22  	"github.com/juju/worker/v3"
    23  	"github.com/juju/worker/v3/workertest"
    24  	"github.com/prometheus/client_golang/prometheus"
    25  	gc "gopkg.in/check.v1"
    26  
    27  	"github.com/juju/juju/core/presence"
    28  	"github.com/juju/juju/pubsub/agent"
    29  	_ "github.com/juju/juju/state"
    30  	"github.com/juju/juju/worker/introspection"
    31  )
    32  
    33  type suite struct {
    34  	testing.IsolationSuite
    35  }
    36  
    37  var _ = gc.Suite(&suite{})
    38  
    39  func (s *suite) TestConfigValidation(c *gc.C) {
    40  	w, err := introspection.NewWorker(introspection.Config{})
    41  	c.Check(w, gc.IsNil)
    42  	c.Assert(err, gc.ErrorMatches, "empty SocketName not valid")
    43  	w, err = introspection.NewWorker(introspection.Config{
    44  		SocketName: "socket",
    45  	})
    46  	c.Check(w, gc.IsNil)
    47  	c.Assert(err, gc.ErrorMatches, "nil PrometheusGatherer not valid")
    48  	w, err = introspection.NewWorker(introspection.Config{
    49  		SocketName:         "socket",
    50  		PrometheusGatherer: newPrometheusGatherer(),
    51  		LocalHub:           pubsub.NewSimpleHub(&pubsub.SimpleHubConfig{}),
    52  	})
    53  	c.Check(w, gc.IsNil)
    54  	c.Assert(err, gc.ErrorMatches, "nil Clock not valid")
    55  }
    56  
    57  func (s *suite) TestStartStop(c *gc.C) {
    58  	if runtime.GOOS != "linux" {
    59  		c.Skip("introspection worker not supported on non-linux")
    60  	}
    61  
    62  	w, err := introspection.NewWorker(introspection.Config{
    63  		SocketName:         "introspection-test",
    64  		PrometheusGatherer: prometheus.NewRegistry(),
    65  	})
    66  	c.Assert(err, jc.ErrorIsNil)
    67  	workertest.CheckKill(c, w)
    68  }
    69  
    70  type introspectionSuite struct {
    71  	testing.IsolationSuite
    72  
    73  	name       string
    74  	worker     worker.Worker
    75  	reporter   introspection.DepEngineReporter
    76  	gatherer   prometheus.Gatherer
    77  	recorder   presence.Recorder
    78  	localHub   *pubsub.SimpleHub
    79  	centralHub introspection.StructuredHub
    80  	clock      *testclock.Clock
    81  }
    82  
    83  var _ = gc.Suite(&introspectionSuite{})
    84  
    85  func (s *introspectionSuite) SetUpTest(c *gc.C) {
    86  	if runtime.GOOS != "linux" {
    87  		c.Skip("introspection worker not supported on non-linux")
    88  	}
    89  	s.IsolationSuite.SetUpTest(c)
    90  	s.reporter = nil
    91  	s.worker = nil
    92  	s.recorder = nil
    93  	s.gatherer = newPrometheusGatherer()
    94  	s.localHub = pubsub.NewSimpleHub(&pubsub.SimpleHubConfig{Logger: loggo.GetLogger("test.localhub")})
    95  	s.centralHub = pubsub.NewStructuredHub(&pubsub.StructuredHubConfig{Logger: loggo.GetLogger("test.centralhub")})
    96  	s.clock = testclock.NewClock(time.Now())
    97  	s.startWorker(c)
    98  }
    99  
   100  func (s *introspectionSuite) startWorker(c *gc.C) {
   101  	s.name = fmt.Sprintf("introspection-test-%d", os.Getpid())
   102  	w, err := introspection.NewWorker(introspection.Config{
   103  		SocketName:         s.name,
   104  		DepEngine:          s.reporter,
   105  		PrometheusGatherer: s.gatherer,
   106  		Presence:           s.recorder,
   107  		Clock:              s.clock,
   108  		LocalHub:           s.localHub,
   109  		CentralHub:         s.centralHub,
   110  	})
   111  	c.Assert(err, jc.ErrorIsNil)
   112  	s.worker = w
   113  	s.AddCleanup(func(c *gc.C) {
   114  		workertest.CleanKill(c, w)
   115  	})
   116  }
   117  
   118  func (s *introspectionSuite) call(c *gc.C, path string) *http.Response {
   119  	client := unixSocketHTTPClient("@" + s.name)
   120  	c.Assert(strings.HasPrefix(path, "/"), jc.IsTrue)
   121  	targetURL, err := url.Parse("http://unix.socket" + path)
   122  	c.Assert(err, jc.ErrorIsNil)
   123  
   124  	resp, err := client.Get(targetURL.String())
   125  	c.Assert(err, jc.ErrorIsNil)
   126  	return resp
   127  }
   128  
   129  func (s *introspectionSuite) post(c *gc.C, path string, values url.Values) *http.Response {
   130  	client := unixSocketHTTPClient("@" + s.name)
   131  	c.Assert(strings.HasPrefix(path, "/"), jc.IsTrue)
   132  	targetURL, err := url.Parse("http://unix.socket" + path)
   133  	c.Assert(err, jc.ErrorIsNil)
   134  
   135  	resp, err := client.PostForm(targetURL.String(), values)
   136  	c.Assert(err, jc.ErrorIsNil)
   137  	return resp
   138  }
   139  
   140  func (s *introspectionSuite) body(c *gc.C, r *http.Response) string {
   141  	response, err := io.ReadAll(r.Body)
   142  	c.Assert(err, jc.ErrorIsNil)
   143  	return string(response)
   144  }
   145  
   146  func (s *introspectionSuite) assertBody(c *gc.C, response *http.Response, value string) {
   147  	body := s.body(c, response)
   148  	c.Assert(body, gc.Equals, value+"\n")
   149  }
   150  
   151  func (s *introspectionSuite) assertContains(c *gc.C, value, expected string) {
   152  	c.Assert(strings.Contains(value, expected), jc.IsTrue,
   153  		gc.Commentf("missing %q in %v", expected, value))
   154  }
   155  
   156  func (s *introspectionSuite) assertBodyContains(c *gc.C, response *http.Response, value string) {
   157  	body := s.body(c, response)
   158  	s.assertContains(c, body, value)
   159  }
   160  
   161  func (s *introspectionSuite) TestCmdLine(c *gc.C) {
   162  	response := s.call(c, "/debug/pprof/cmdline")
   163  	s.assertBodyContains(c, response, "/introspection.test")
   164  }
   165  
   166  func (s *introspectionSuite) TestGoroutineProfile(c *gc.C) {
   167  	response := s.call(c, "/debug/pprof/goroutine?debug=1")
   168  	body := s.body(c, response)
   169  	c.Check(body, gc.Matches, `(?s)^goroutine profile: total \d+.*`)
   170  }
   171  
   172  func (s *introspectionSuite) TestTrace(c *gc.C) {
   173  	response := s.call(c, "/debug/pprof/trace?seconds=1")
   174  	c.Assert(response.Header.Get("Content-Type"), gc.Equals, "application/octet-stream")
   175  }
   176  
   177  func (s *introspectionSuite) TestMissingDepEngineReporter(c *gc.C) {
   178  	response := s.call(c, "/depengine")
   179  	c.Assert(response.StatusCode, gc.Equals, http.StatusNotFound)
   180  	s.assertBody(c, response, "missing dependency engine reporter")
   181  }
   182  
   183  func (s *introspectionSuite) TestMissingStatePoolReporter(c *gc.C) {
   184  	response := s.call(c, "/statepool")
   185  	c.Assert(response.StatusCode, gc.Equals, http.StatusNotFound)
   186  	s.assertBody(c, response, `"State Pool" introspection not supported`)
   187  }
   188  
   189  func (s *introspectionSuite) TestMissingPubSubReporter(c *gc.C) {
   190  	response := s.call(c, "/pubsub")
   191  	c.Assert(response.StatusCode, gc.Equals, http.StatusNotFound)
   192  	s.assertBody(c, response, `"PubSub Report" introspection not supported`)
   193  }
   194  
   195  func (s *introspectionSuite) TestMissingMachineLock(c *gc.C) {
   196  	response := s.call(c, "/machinelock")
   197  	c.Assert(response.StatusCode, gc.Equals, http.StatusNotFound)
   198  	s.assertBody(c, response, "missing machine lock reporter")
   199  }
   200  
   201  func (s *introspectionSuite) TestStateTrackerReporter(c *gc.C) {
   202  	response := s.call(c, "/debug/pprof/juju/state/tracker?debug=1")
   203  	c.Assert(response.StatusCode, gc.Equals, http.StatusOK)
   204  	s.assertBodyContains(c, response, "juju/state/tracker profile: total")
   205  }
   206  
   207  func (s *introspectionSuite) TestEngineReporter(c *gc.C) {
   208  	// We need to make sure the existing worker is shut down
   209  	// so we can connect to the socket.
   210  	workertest.CheckKill(c, s.worker)
   211  	s.reporter = &reporter{
   212  		values: map[string]interface{}{
   213  			"working": true,
   214  		},
   215  	}
   216  	s.startWorker(c)
   217  	response := s.call(c, "/depengine")
   218  	c.Assert(response.StatusCode, gc.Equals, http.StatusOK)
   219  	// TODO: perhaps make the output of the dependency engine YAML parseable.
   220  	// This could be done by having the first line start with a '#'.
   221  	s.assertBody(c, response, `
   222  Dependency Engine Report
   223  
   224  working: true`[1:])
   225  }
   226  
   227  func (s *introspectionSuite) TestMissingPresenceReporter(c *gc.C) {
   228  	response := s.call(c, "/presence")
   229  	c.Assert(response.StatusCode, gc.Equals, http.StatusNotFound)
   230  	s.assertBody(c, response, `"Presence" introspection not supported`)
   231  }
   232  
   233  func (s *introspectionSuite) TestDisabledPresenceReporter(c *gc.C) {
   234  	// We need to make sure the existing worker is shut down
   235  	// so we can connect to the socket.
   236  	workertest.CheckKill(c, s.worker)
   237  	s.recorder = presence.New(testclock.NewClock(time.Now()))
   238  	s.startWorker(c)
   239  
   240  	response := s.call(c, "/presence")
   241  	c.Assert(response.StatusCode, gc.Equals, http.StatusNotFound)
   242  	s.assertBody(c, response, "agent is not an apiserver")
   243  }
   244  
   245  func (s *introspectionSuite) TestEnabledPresenceReporter(c *gc.C) {
   246  	// We need to make sure the existing worker is shut down
   247  	// so we can connect to the socket.
   248  	workertest.CheckKill(c, s.worker)
   249  	s.recorder = presence.New(testclock.NewClock(time.Now()))
   250  	s.recorder.Enable()
   251  	s.recorder.Connect("server", "model-uuid", "agent-1", 42, false, "")
   252  	s.startWorker(c)
   253  
   254  	response := s.call(c, "/presence")
   255  	c.Assert(response.StatusCode, gc.Equals, http.StatusOK)
   256  	s.assertBody(c, response, `
   257  [model-uuid]
   258  
   259  AGENT    SERVER  CONN ID  STATUS
   260  agent-1  server  42       alive
   261  `[1:])
   262  }
   263  
   264  func (s *introspectionSuite) TestPrometheusMetrics(c *gc.C) {
   265  	response := s.call(c, "/metrics")
   266  	c.Assert(response.StatusCode, gc.Equals, http.StatusOK)
   267  	body := s.body(c, response)
   268  	s.assertContains(c, body, "# HELP tau Tau")
   269  	s.assertContains(c, body, "# TYPE tau counter")
   270  	s.assertContains(c, body, "tau 6.283185")
   271  }
   272  
   273  func (s *introspectionSuite) TestUnitMissingAction(c *gc.C) {
   274  	response := s.call(c, "/units")
   275  	c.Assert(response.StatusCode, gc.Equals, http.StatusBadRequest)
   276  	s.assertBody(c, response, "missing action")
   277  }
   278  
   279  func (s *introspectionSuite) TestUnitUnknownAction(c *gc.C) {
   280  	response := s.post(c, "/units", url.Values{"action": {"foo"}})
   281  	c.Assert(response.StatusCode, gc.Equals, http.StatusBadRequest)
   282  	s.assertBody(c, response, `unknown action: "foo"`)
   283  }
   284  
   285  func (s *introspectionSuite) TestUnitStartWithGet(c *gc.C) {
   286  	response := s.call(c, "/units?action=start")
   287  	c.Assert(response.StatusCode, gc.Equals, http.StatusMethodNotAllowed)
   288  	s.assertBody(c, response, `start requires a POST request, got "GET"`)
   289  }
   290  
   291  func (s *introspectionSuite) TestUnitStartMissingUnits(c *gc.C) {
   292  	response := s.post(c, "/units", url.Values{"action": {"start"}})
   293  	c.Assert(response.StatusCode, gc.Equals, http.StatusBadRequest)
   294  	s.assertBody(c, response, "missing unit")
   295  }
   296  
   297  func (s *introspectionSuite) TestUnitStartUnits(c *gc.C) {
   298  	unsub := s.localHub.Subscribe(agent.StartUnitTopic, func(topic string, data interface{}) {
   299  		_, ok := data.(agent.Units)
   300  		if !ok {
   301  			c.Fatalf("bad data type: %T", data)
   302  			return
   303  		}
   304  		s.localHub.Publish(agent.StartUnitResponseTopic, agent.StartStopResponse{
   305  			"one": "started",
   306  			"two": "not found",
   307  		})
   308  	})
   309  	defer unsub()
   310  
   311  	response := s.post(c, "/units", url.Values{"action": {"start"}, "unit": {"one", "two"}})
   312  	c.Assert(response.StatusCode, gc.Equals, http.StatusOK)
   313  	s.assertBody(c, response, "one: started\ntwo: not found")
   314  }
   315  
   316  func (s *introspectionSuite) TestUnitStopWithGet(c *gc.C) {
   317  	response := s.call(c, "/units?action=stop")
   318  	c.Assert(response.StatusCode, gc.Equals, http.StatusMethodNotAllowed)
   319  	s.assertBody(c, response, `stop requires a POST request, got "GET"`)
   320  }
   321  
   322  func (s *introspectionSuite) TestUnitStopMissingUnits(c *gc.C) {
   323  	response := s.post(c, "/units", url.Values{"action": {"stop"}})
   324  	c.Assert(response.StatusCode, gc.Equals, http.StatusBadRequest)
   325  	s.assertBody(c, response, "missing unit")
   326  }
   327  
   328  func (s *introspectionSuite) TestUnitStopUnits(c *gc.C) {
   329  	unsub := s.localHub.Subscribe(agent.StopUnitTopic, func(topic string, data interface{}) {
   330  		_, ok := data.(agent.Units)
   331  		if !ok {
   332  			c.Fatalf("bad data type: %T", data)
   333  			return
   334  		}
   335  		s.localHub.Publish(agent.StopUnitResponseTopic, agent.StartStopResponse{
   336  			"one": "stopped",
   337  			"two": "not found",
   338  		})
   339  	})
   340  	defer unsub()
   341  
   342  	response := s.post(c, "/units", url.Values{"action": {"stop"}, "unit": {"one", "two"}})
   343  	c.Assert(response.StatusCode, gc.Equals, http.StatusOK)
   344  	s.assertBody(c, response, "one: stopped\ntwo: not found")
   345  }
   346  
   347  func (s *introspectionSuite) TestUnitStatus(c *gc.C) {
   348  	unsub := s.localHub.Subscribe(agent.UnitStatusTopic, func(string, interface{}) {
   349  		s.localHub.Publish(agent.UnitStatusResponseTopic, agent.Status{
   350  			"one": "running",
   351  			"two": "stopped",
   352  		})
   353  	})
   354  	defer unsub()
   355  
   356  	response := s.call(c, "/units?action=status")
   357  	c.Assert(response.StatusCode, gc.Equals, http.StatusOK)
   358  	s.assertBody(c, response, `
   359  one: running
   360  two: stopped`[1:])
   361  }
   362  
   363  func (s *introspectionSuite) TestUnitStatusTimeout(c *gc.C) {
   364  	unsub := s.localHub.Subscribe(agent.UnitStatusTopic, func(string, interface{}) {
   365  		s.clock.WaitAdvance(10*time.Second, time.Second, 1)
   366  	})
   367  	defer unsub()
   368  
   369  	response := s.call(c, "/units?action=status")
   370  	c.Assert(response.StatusCode, gc.Equals, http.StatusInternalServerError)
   371  	s.assertBody(c, response, "response timed out")
   372  }
   373  
   374  type reporter struct {
   375  	values map[string]interface{}
   376  }
   377  
   378  func (r *reporter) Report() map[string]interface{} {
   379  	return r.values
   380  }
   381  
   382  func newPrometheusGatherer() prometheus.Gatherer {
   383  	counter := prometheus.NewCounter(prometheus.CounterOpts{Name: "tau", Help: "Tau."})
   384  	counter.Add(6.283185)
   385  	r := prometheus.NewPedanticRegistry()
   386  	r.MustRegister(counter)
   387  	return r
   388  }
   389  
   390  func unixSocketHTTPClient(socketPath string) *http.Client {
   391  	return &http.Client{
   392  		Transport: unixSocketHTTPTransport(socketPath),
   393  		Timeout:   15 * time.Second,
   394  	}
   395  }
   396  
   397  func unixSocketHTTPTransport(socketPath string) *http.Transport {
   398  	return &http.Transport{
   399  		Dial: func(proto, addr string) (net.Conn, error) {
   400  			return net.Dial("unix", socketPath)
   401  		},
   402  	}
   403  }