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 }