github.com/sleungcy/cli@v7.1.0+incompatible/actor/v2action/logging_test.go (about) 1 package v2action_test 2 3 import ( 4 "context" 5 "errors" 6 "time" 7 8 "code.cloudfoundry.org/cli/actor/sharedaction" 9 "code.cloudfoundry.org/cli/actor/sharedaction/sharedactionfakes" 10 . "code.cloudfoundry.org/cli/actor/v2action" 11 "code.cloudfoundry.org/cli/actor/v2action/v2actionfakes" 12 "code.cloudfoundry.org/cli/api/cloudcontroller/ccv2" 13 "code.cloudfoundry.org/cli/api/uaa" 14 "code.cloudfoundry.org/cli/integration/helpers" 15 logcache "code.cloudfoundry.org/go-log-cache" 16 "code.cloudfoundry.org/go-loggregator/rpc/loggregator_v2" 17 . "github.com/onsi/ginkgo" 18 . "github.com/onsi/gomega" 19 ) 20 21 var _ = Describe("Logging Actions", func() { 22 var ( 23 actor *Actor 24 fakeCloudControllerClient *v2actionfakes.FakeCloudControllerClient 25 fakeUAAClient *v2actionfakes.FakeUAAClient 26 fakeConfig *v2actionfakes.FakeConfig 27 28 fakeLogCacheClient *sharedactionfakes.FakeLogCacheClient 29 ) 30 31 BeforeEach(func() { 32 actor, fakeCloudControllerClient, fakeUAAClient, fakeConfig = NewTestActor() 33 fakeLogCacheClient = new(sharedactionfakes.FakeLogCacheClient) 34 fakeConfig.AccessTokenReturns("AccessTokenForTest") 35 }) 36 37 Describe("LogMessage", func() { 38 Describe("Staging", func() { 39 When("the log is a staging log", func() { 40 It("returns true", func() { 41 message := sharedaction.NewLogMessage("", "OUT", time.Now(), "STG", "") 42 Expect(message.Staging()).To(BeTrue()) 43 }) 44 }) 45 46 When("the log is any other kind of log", func() { 47 It("returns true", func() { 48 message := sharedaction.NewLogMessage("", "OUT", time.Now(), "APP", "") 49 Expect(message.Staging()).To(BeFalse()) 50 }) 51 }) 52 }) 53 }) 54 55 Describe("GetStreamingLogs", func() { 56 var ( 57 expectedAppGUID string 58 59 messages <-chan sharedaction.LogMessage 60 errs <-chan error 61 stopStreaming context.CancelFunc 62 63 message sharedaction.LogMessage 64 ) 65 66 BeforeEach(func() { 67 expectedAppGUID = "some-app-guid" 68 }) 69 70 AfterEach(func() { 71 Eventually(messages).Should(BeClosed()) 72 Eventually(errs).Should(BeClosed()) 73 }) 74 75 JustBeforeEach(func() { 76 messages, errs, stopStreaming = actor.GetStreamingLogs(expectedAppGUID, fakeLogCacheClient) 77 }) 78 79 When("receiving events", func() { 80 BeforeEach(func() { 81 fakeConfig.DialTimeoutReturns(60 * time.Minute) 82 fakeLogCacheClient.ReadStub = func( 83 ctx context.Context, 84 sourceID string, 85 start time.Time, 86 opts ...logcache.ReadOption, 87 ) ([]*loggregator_v2.Envelope, error) { 88 if fakeLogCacheClient.ReadCallCount() > 2 { 89 stopStreaming() 90 } 91 92 return []*loggregator_v2.Envelope{{ 93 // 3 seconds in the past to get past Walk delay 94 Timestamp: time.Now().Add(-4 * time.Second).UnixNano(), 95 SourceId: expectedAppGUID, 96 InstanceId: "some-source-instance", 97 Message: &loggregator_v2.Envelope_Log{ 98 Log: &loggregator_v2.Log{ 99 Payload: []byte("message-1"), 100 Type: loggregator_v2.Log_OUT, 101 }, 102 }, 103 Tags: map[string]string{ 104 "source_type": "some-source-type", 105 }, 106 }, { 107 // 2 seconds in the past to get past Walk delay 108 Timestamp: time.Now().Add(-3 * time.Second).UnixNano(), 109 SourceId: expectedAppGUID, 110 InstanceId: "some-source-instance", 111 Message: &loggregator_v2.Envelope_Log{ 112 Log: &loggregator_v2.Log{ 113 Payload: []byte("message-2"), 114 Type: loggregator_v2.Log_OUT, 115 }, 116 }, 117 Tags: map[string]string{ 118 "source_type": "some-source-type", 119 }, 120 }, { 121 Timestamp: time.Now().Add(-2005 * time.Millisecond).UnixNano(), 122 SourceId: expectedAppGUID, 123 InstanceId: "some-source-instance", 124 Message: &loggregator_v2.Envelope_Log{ 125 Log: &loggregator_v2.Log{ 126 Payload: []byte("message-3"), 127 Type: loggregator_v2.Log_OUT, 128 }, 129 }, 130 Tags: map[string]string{ 131 "source_type": "some-source-type", 132 }, 133 }, { 134 Timestamp: time.Now().Add(-1900 * time.Millisecond).UnixNano(), 135 SourceId: expectedAppGUID, 136 InstanceId: "some-source-instance", 137 Message: &loggregator_v2.Envelope_Log{ 138 Log: &loggregator_v2.Log{ 139 Payload: []byte("message-4"), 140 Type: loggregator_v2.Log_OUT, 141 }, 142 }, 143 Tags: map[string]string{ 144 "source_type": "some-source-type", 145 }, 146 }, 147 }, 148 ctx.Err() 149 } 150 }) 151 152 It("converts them to log messages, does not sort them, and passes them through the messages channel", func() { 153 Eventually(messages).Should(Receive(&message)) 154 Expect(message.Message()).To(Equal("message-1")) 155 Expect(message.Type()).To(Equal("OUT")) 156 157 Eventually(messages).Should(Receive(&message)) 158 Expect(message.Message()).To(Equal("message-2")) 159 Expect(message.Type()).To(Equal("OUT")) 160 161 Eventually(messages).Should(Receive(&message)) 162 Expect(message.Message()).To(Equal("message-3")) 163 Expect(message.Type()).To(Equal("OUT")) 164 165 Expect(message.SourceType()).To(Equal("some-source-type")) 166 Expect(message.SourceInstance()).To(Equal("some-source-instance")) 167 168 // The last message in the list is ignored because of the new WalkDelay of 2 seconds 169 Consistently(messages).ShouldNot(Receive(&message)) 170 }) 171 }) 172 }) 173 174 Describe("GetRecentLogsForApplicationByNameAndSpace", func() { 175 When("the application can be found", func() { 176 BeforeEach(func() { 177 fakeCloudControllerClient.GetApplicationsReturns( 178 []ccv2.Application{ 179 { 180 Name: "some-app", 181 GUID: "some-app-guid", 182 }, 183 }, 184 ccv2.Warnings{"some-app-warnings"}, 185 nil, 186 ) 187 }) 188 189 When("LogCache returns logs", func() { 190 BeforeEach(func() { 191 fakeConfig.DialTimeoutReturns(60 * time.Minute) 192 193 fakeLogCacheClient.ReadReturns([]*loggregator_v2.Envelope{{ 194 // 2 seconds in the past to get past Walk delay 195 Timestamp: time.Now().Add(-3 * time.Second).UnixNano(), 196 SourceId: "some-app-guid", 197 InstanceId: "some-source-instance", 198 Message: &loggregator_v2.Envelope_Log{ 199 Log: &loggregator_v2.Log{ 200 Payload: []byte("message-1"), 201 Type: loggregator_v2.Log_OUT, 202 }, 203 }, 204 Tags: map[string]string{ 205 "source_type": "some-source-type", 206 }, 207 }, { 208 // 2 seconds in the past to get past Walk delay 209 Timestamp: time.Now().Add(-2 * time.Second).UnixNano(), 210 SourceId: "some-app-guid", 211 InstanceId: "some-source-instance", 212 Message: &loggregator_v2.Envelope_Log{ 213 Log: &loggregator_v2.Log{ 214 Payload: []byte("message-2"), 215 Type: loggregator_v2.Log_OUT, 216 }, 217 }, 218 Tags: map[string]string{ 219 "source_type": "some-source-type", 220 }, 221 }}, 222 nil, 223 ) 224 }) 225 226 It("returns all the recent logs and warnings", func() { 227 messages, warnings, err := actor.GetRecentLogsForApplicationByNameAndSpace("some-app", "some-space-guid", fakeLogCacheClient) 228 Expect(err).ToNot(HaveOccurred()) 229 Expect(warnings).To(ConsistOf("some-app-warnings")) 230 Expect(messages[0].Message()).To(Equal("message-2")) 231 Expect(messages[0].Type()).To(Equal("OUT")) 232 Expect(messages[0].SourceType()).To(Equal("some-source-type")) 233 Expect(messages[0].SourceInstance()).To(Equal("some-source-instance")) 234 235 Expect(messages[1].Message()).To(Equal("message-1")) 236 Expect(messages[1].Type()).To(Equal("OUT")) 237 Expect(messages[1].SourceType()).To(Equal("some-source-type")) 238 Expect(messages[1].SourceInstance()).To(Equal("some-source-instance")) 239 }) 240 }) 241 242 When("LogCache errors", func() { 243 var expectedErr error 244 245 BeforeEach(func() { 246 expectedErr = errors.New("ZOMG") 247 fakeLogCacheClient.ReadReturns(nil, expectedErr) 248 }) 249 250 It("returns error and warnings", func() { 251 _, warnings, err := actor.GetRecentLogsForApplicationByNameAndSpace("some-app", "some-space-guid", fakeLogCacheClient) 252 Expect(err).To(MatchError("Failed to retrieve logs from Log Cache: ZOMG")) 253 Expect(warnings).To(ConsistOf("some-app-warnings")) 254 }) 255 }) 256 }) 257 258 When("finding the application errors", func() { 259 var expectedErr error 260 261 BeforeEach(func() { 262 expectedErr = errors.New("ZOMG") 263 fakeCloudControllerClient.GetApplicationsReturns( 264 nil, 265 ccv2.Warnings{"some-app-warnings"}, 266 expectedErr, 267 ) 268 }) 269 270 It("returns error and warnings", func() { 271 _, warnings, err := actor.GetRecentLogsForApplicationByNameAndSpace("some-app", "some-space-guid", fakeLogCacheClient) 272 Expect(err).To(MatchError(expectedErr)) 273 Expect(warnings).To(ConsistOf("some-app-warnings")) 274 275 Expect(fakeLogCacheClient.ReadCallCount()).To(Equal(0)) 276 }) 277 }) 278 }) 279 280 Describe("GetStreamingLogsForApplicationByNameAndSpace", func() { 281 When("the application can be found", func() { 282 var ( 283 expectedAppGUID string 284 285 messages <-chan sharedaction.LogMessage 286 logErrs <-chan error 287 stopStreaming context.CancelFunc 288 ) 289 290 AfterEach(func() { 291 Eventually(messages).Should(BeClosed()) 292 Eventually(logErrs).Should(BeClosed()) 293 }) 294 295 BeforeEach(func() { 296 expectedAppGUID = "some-app-guid" 297 298 fakeCloudControllerClient.GetApplicationsReturns( 299 []ccv2.Application{ 300 { 301 Name: "some-app", 302 GUID: expectedAppGUID, 303 }, 304 }, 305 ccv2.Warnings{"some-app-warnings"}, 306 nil, 307 ) 308 309 fakeConfig.DialTimeoutReturns(60 * time.Minute) 310 311 fakeLogCacheClient.ReadStub = func( 312 ctx context.Context, 313 sourceID string, 314 start time.Time, 315 opts ...logcache.ReadOption, 316 ) ([]*loggregator_v2.Envelope, error) { 317 if fakeLogCacheClient.ReadCallCount() > 2 { 318 stopStreaming() 319 } 320 321 return []*loggregator_v2.Envelope{{ 322 // 2 seconds in the past to get past Walk delay 323 Timestamp: time.Now().Add(-3 * time.Second).UnixNano(), 324 SourceId: expectedAppGUID, 325 InstanceId: "some-source-instance", 326 Message: &loggregator_v2.Envelope_Log{ 327 Log: &loggregator_v2.Log{ 328 Payload: []byte("message-1"), 329 Type: loggregator_v2.Log_OUT, 330 }, 331 }, 332 Tags: map[string]string{ 333 "source_type": "some-source-type", 334 }, 335 }, { 336 // 2 seconds in the past to get past Walk delay 337 Timestamp: time.Now().Add(-2 * time.Second).UnixNano(), 338 SourceId: expectedAppGUID, 339 InstanceId: "some-source-instance", 340 Message: &loggregator_v2.Envelope_Log{ 341 Log: &loggregator_v2.Log{ 342 Payload: []byte("message-2"), 343 Type: loggregator_v2.Log_ERR, 344 }, 345 }, 346 Tags: map[string]string{ 347 "source_type": "some-source-type", 348 }, 349 }}, ctx.Err() 350 } 351 }) 352 353 It("converts them to log messages and passes them through the messages channel", func() { 354 var err error 355 var warnings Warnings 356 var message sharedaction.LogMessage 357 358 messages, logErrs, stopStreaming, warnings, err = actor.GetStreamingLogsForApplicationByNameAndSpace("some-app", "some-space-guid", fakeLogCacheClient) 359 360 Expect(err).ToNot(HaveOccurred()) 361 Expect(warnings).To(ConsistOf("some-app-warnings")) 362 363 Eventually(messages).Should(Receive(&message)) 364 Expect(message.Message()).To(Equal("message-1")) 365 Expect(message.Type()).To(Equal("OUT")) 366 Expect(message.SourceType()).To(Equal("some-source-type")) 367 Expect(message.SourceInstance()).To(Equal("some-source-instance")) 368 369 Eventually(messages).Should(Receive(&message)) 370 Expect(message.Message()).To(Equal("message-2")) 371 Expect(message.Type()).To(Equal("ERR")) 372 Expect(message.SourceType()).To(Equal("some-source-type")) 373 Expect(message.SourceInstance()).To(Equal("some-source-instance")) 374 }) 375 }) 376 377 When("finding the application errors", func() { 378 var expectedErr error 379 380 BeforeEach(func() { 381 expectedErr = errors.New("ZOMG") 382 fakeCloudControllerClient.GetApplicationsReturns( 383 nil, 384 ccv2.Warnings{"some-app-warnings"}, 385 expectedErr, 386 ) 387 }) 388 389 It("returns error and warnings", func() { 390 _, _, _, warnings, err := actor.GetStreamingLogsForApplicationByNameAndSpace("some-app", "some-space-guid", fakeLogCacheClient) 391 Expect(err).To(MatchError(expectedErr)) 392 Expect(warnings).To(ConsistOf("some-app-warnings")) 393 394 Expect(fakeLogCacheClient.ReadCallCount()).To(Equal(0)) 395 }) 396 }) 397 }) 398 399 Describe("ScheduleTokenRefresh", func() { 400 var ( 401 stop chan struct{} 402 stoppedRefreshingToken chan struct{} 403 ticker chan time.Time 404 after func(time.Duration) <-chan time.Time 405 delay chan time.Duration 406 errChannel <-chan error 407 err error 408 ) 409 410 BeforeEach(func() { 411 fakeConfig.RefreshTokenReturns(helpers.BuildTokenString(time.Now().Add(5 * time.Minute))) 412 ticker = make(chan time.Time) 413 delay = make(chan time.Duration, 100) 414 after = func(t time.Duration) <-chan time.Time { 415 delay <- t 416 return ticker 417 } 418 }) 419 420 JustBeforeEach(func() { 421 stop = make(chan struct{}) 422 stoppedRefreshingToken = make(chan struct{}) 423 errChannel, err = actor.ScheduleTokenRefresh(after, stop, stoppedRefreshingToken) 424 }) 425 426 AfterEach(func() { 427 if stop != nil { 428 close(stop) 429 } 430 }) 431 432 When("the access token is not expiring soon", func() { 433 BeforeEach(func() { 434 fakeConfig.AccessTokenReturns(helpers.BuildTokenString(time.Now().Add(5 * time.Minute))) 435 }) 436 437 It("does not refresh the access token", func() { 438 Expect(err).NotTo(HaveOccurred()) 439 Expect(fakeUAAClient.RefreshAccessTokenCallCount()).To(Equal(0)) 440 }) 441 }) 442 443 When("the access token is expiring soon", func() { 444 BeforeEach(func() { 445 fakeConfig.AccessTokenReturns(helpers.BuildTokenString(time.Now().Add(30 * time.Second))) 446 fakeUAAClient.RefreshAccessTokenReturns(uaa.RefreshedTokens{ 447 AccessToken: helpers.BuildTokenString(time.Now().Add(5 * time.Minute)), 448 Type: "bearer", 449 }, nil) 450 }) 451 452 It("refreshes the access token", func() { 453 Expect(err).NotTo(HaveOccurred()) 454 Expect(fakeUAAClient.RefreshAccessTokenCallCount()).To(Equal(1)) 455 }) 456 }) 457 458 When("the access token has already expired", func() { 459 BeforeEach(func() { 460 fakeConfig.AccessTokenReturns(helpers.BuildTokenString(time.Now().Add(-30 * time.Second))) 461 fakeUAAClient.RefreshAccessTokenReturns(uaa.RefreshedTokens{ 462 AccessToken: helpers.BuildTokenString(time.Now().Add(5 * time.Minute)), 463 Type: "bearer", 464 }, nil) 465 }) 466 467 It("refreshes the access token", func() { 468 Expect(err).NotTo(HaveOccurred()) 469 Expect(fakeUAAClient.RefreshAccessTokenCallCount()).To(Equal(1)) 470 }) 471 472 When("and the attempt to refresh the access token fails", func() { 473 BeforeEach(func() { 474 fakeUAAClient.RefreshAccessTokenReturns(uaa.RefreshedTokens{}, errors.New("some error")) 475 }) 476 477 It("will not refresh the access token", func() { 478 Expect(err).To(MatchError("some error")) 479 }) 480 It("closes a channel to indicate it has finished", func() { 481 close(stop) 482 Eventually(stoppedRefreshingToken).Should(BeClosed()) 483 }) 484 AfterEach(func() { 485 stop = nil 486 }) 487 }) 488 }) 489 490 When("the access token is invalid", func() { 491 BeforeEach(func() { 492 fakeConfig.AccessTokenReturns("bogus-access-token") 493 fakeUAAClient.RefreshAccessTokenReturns(uaa.RefreshedTokens{ 494 AccessToken: helpers.BuildTokenString(time.Now().Add(5 * time.Minute)), 495 Type: "bearer", 496 }, nil) 497 }) 498 It("refreshes the access token", func() { 499 Expect(err).NotTo(HaveOccurred()) 500 Expect(fakeUAAClient.RefreshAccessTokenCallCount()).To(Equal(1)) 501 }) 502 }) 503 504 When("the access token expires while we are streaming logs", func() { 505 506 BeforeEach(func() { 507 fakeConfig.AccessTokenReturns(helpers.BuildTokenString(time.Now().Add(2 * time.Minute))) 508 fakeConfig.RefreshTokenReturns(helpers.BuildTokenString(time.Now().Add(2 * time.Minute))) 509 }) 510 511 JustBeforeEach(func() { 512 stop = make(chan struct{}) 513 stoppedRefreshingToken = make(chan struct{}) 514 errChannel, err = actor.ScheduleTokenRefresh(after, stop, stoppedRefreshingToken) 515 }) 516 517 It("refreshes the access token", func() { 518 Expect(err).NotTo(HaveOccurred()) 519 520 By("not initially refreshing the token when it is unnecessary") 521 Expect(fakeUAAClient.RefreshAccessTokenCallCount()).To(Equal(0)) 522 523 By("refreshing the first access token when it is expiring soon") 524 fakeConfig.AccessTokenReturns(helpers.BuildTokenString(time.Now().Add(30 * time.Second))) 525 ticker <- time.Time{} 526 Eventually(fakeUAAClient.RefreshAccessTokenCallCount).Should(Equal(1)) 527 528 By("not refreshing the second access token when it is not close to expiry") 529 fakeConfig.AccessTokenReturns(helpers.BuildTokenString(time.Now().Add(90 * time.Second))) 530 ticker <- time.Time{} 531 Consistently(fakeUAAClient.RefreshAccessTokenCallCount).Should(Equal(1)) 532 533 By("refreshing the second access token when it is near to expiry") 534 fakeConfig.AccessTokenReturns(helpers.BuildTokenString(time.Now().Add(30 * time.Second))) 535 ticker <- time.Time{} 536 Eventually(fakeUAAClient.RefreshAccessTokenCallCount).Should(Equal(2)) 537 }) 538 539 It("sleeps until the token is approaching expiry", func() { 540 fakeConfig.AccessTokenReturns(helpers.BuildTokenString(time.Now().Add(30 * time.Second))) 541 Expect(err).NotTo(HaveOccurred()) 542 543 By("sleeps until one minute before the new token is due to expire") 544 fakeUAAClient.RefreshAccessTokenReturns(uaa.RefreshedTokens{ 545 AccessToken: helpers.BuildTokenString(time.Now().Add(5 * time.Minute)), 546 Type: "bearer", 547 }, nil) 548 549 ticker <- time.Time{} 550 Eventually(fakeUAAClient.RefreshAccessTokenCallCount).Should(Equal(1)) 551 Eventually(delay).Should(Receive(BeNumerically("~", 4*time.Minute, time.Second))) 552 553 By("refreshing a longer-lived access token") 554 fakeUAAClient.RefreshAccessTokenReturns(uaa.RefreshedTokens{ 555 AccessToken: helpers.BuildTokenString(time.Now().Add(20 * time.Minute)), 556 Type: "bearer", 557 }, nil) 558 ticker <- time.Time{} 559 Eventually(fakeUAAClient.RefreshAccessTokenCallCount).Should(Equal(2)) 560 Eventually(delay).Should(Receive(BeNumerically("~", 19*time.Minute, time.Second))) 561 562 By("refreshing a short-lived access token at 90% of its lifetime") 563 fakeUAAClient.RefreshAccessTokenReturns(uaa.RefreshedTokens{ 564 AccessToken: helpers.BuildTokenString(time.Now().Add(30 * time.Second)), 565 Type: "bearer", 566 }, nil) 567 ticker <- time.Time{} 568 Eventually(fakeUAAClient.RefreshAccessTokenCallCount).Should(Equal(3)) 569 Eventually(delay).Should(Receive(BeNumerically("~", 27*time.Second, time.Second))) 570 571 By("refreshing a very short-lived access token at 90% of its lifetime") 572 fakeUAAClient.RefreshAccessTokenReturns(uaa.RefreshedTokens{ 573 AccessToken: helpers.BuildTokenString(time.Now().Add(3000 * time.Millisecond)), 574 Type: "bearer", 575 }, nil) 576 ticker <- time.Time{} 577 Eventually(fakeUAAClient.RefreshAccessTokenCallCount).Should(Equal(4)) 578 Eventually(delay).Should(Receive(BeNumerically("~", 2700*time.Millisecond, time.Second))) 579 580 By("refreshing a token with a little more than a minute left on it") 581 fakeUAAClient.RefreshAccessTokenReturns(uaa.RefreshedTokens{ 582 AccessToken: helpers.BuildTokenString(time.Now().Add(90 * time.Second)), 583 Type: "bearer", 584 }, nil) 585 ticker <- time.Time{} 586 Eventually(fakeUAAClient.RefreshAccessTokenCallCount).Should(Equal(5)) 587 Eventually(delay).Should(Receive(BeNumerically("~", 81*time.Second, time.Second))) 588 }) 589 590 When("and the token refresh errors", func() { 591 BeforeEach(func() { 592 fakeUAAClient.RefreshAccessTokenReturns(uaa.RefreshedTokens{}, errors.New("some error")) 593 }) 594 It("propagates the error to the caller", func() { 595 Expect(err).NotTo(HaveOccurred()) 596 Expect(fakeUAAClient.RefreshAccessTokenCallCount()).To(Equal(0)) 597 fakeConfig.AccessTokenReturns(helpers.BuildTokenString(time.Now().Add(30 * time.Second))) 598 go func() { 599 defer GinkgoRecover() 600 Eventually(errChannel).Should(Receive(MatchError("some error"))) 601 }() 602 ticker <- time.Time{} 603 Eventually(fakeUAAClient.RefreshAccessTokenCallCount).Should(Equal(1)) 604 }) 605 }) 606 607 When("token refreshing is stopped", func() { 608 It("closes a channel to indicate it has finished", func() { 609 close(stop) 610 Eventually(stoppedRefreshingToken).Should(BeClosed()) 611 }) 612 AfterEach(func() { 613 stop = nil 614 }) 615 }) 616 617 }) 618 619 }) 620 })