diff --git a/ui/app/components/task-log.js b/ui/app/components/task-log.js index b5cda3eed3a..773ef10720b 100644 --- a/ui/app/components/task-log.js +++ b/ui/app/components/task-log.js @@ -5,6 +5,12 @@ import RSVP from 'rsvp'; import { logger } from 'nomad-ui/utils/classes/log'; import timeout from 'nomad-ui/utils/timeout'; +class MockAbortController { + abort() { + /* noop */ + } +} + export default Component.extend({ token: service(), @@ -45,12 +51,25 @@ export default Component.extend({ logger: logger('logUrl', 'logParams', function logFetch() { // If the log request can't settle in one second, the client // must be unavailable and the server should be used instead + + // AbortControllers don't exist in IE11, so provide a mock if it doesn't exist + const aborter = window.AbortController ? new AbortController() : new MockAbortController(); const timing = this.useServer ? this.serverTimeout : this.clientTimeout; + + // Capture the state of useServer at logger create time to avoid a race + // between the stdout logger and stderr logger running at once. + const useServer = this.useServer; return url => - RSVP.race([this.token.authorizedRequest(url), timeout(timing)]).then( - response => response, + RSVP.race([ + this.token.authorizedRequest(url, { signal: aborter.signal }), + timeout(timing), + ]).then( + response => { + return response; + }, error => { - if (this.useServer) { + aborter.abort(); + if (useServer) { this.set('noConnection', true); } else { this.send('failoverToServer'); @@ -62,6 +81,7 @@ export default Component.extend({ actions: { setMode(mode) { + if (this.mode === mode) return; this.logger.stop(); this.set('mode', mode); }, diff --git a/ui/app/services/token.js b/ui/app/services/token.js index 5c251bd6847..256c2646f86 100644 --- a/ui/app/services/token.js +++ b/ui/app/services/token.js @@ -72,7 +72,8 @@ export default Service.extend({ // This authorizedRawRequest is necessary in order to fetch data // with the guarantee of a token but without the automatic region // param since the region cannot be known at this point. - authorizedRawRequest(url, options = { credentials: 'include' }) { + authorizedRawRequest(url, options = {}) { + const credentials = 'include'; const headers = {}; const token = this.secret; @@ -80,7 +81,7 @@ export default Service.extend({ headers['X-Nomad-Token'] = token; } - return fetch(url, assign(options, { headers })); + return fetch(url, assign(options, { headers, credentials })); }, authorizedRequest(url, options) { diff --git a/ui/app/templates/components/task-log.hbs b/ui/app/templates/components/task-log.hbs index 89a363747ae..1dc5caa79f8 100644 --- a/ui/app/templates/components/task-log.hbs +++ b/ui/app/templates/components/task-log.hbs @@ -1,7 +1,14 @@ {{#if noConnection}}
-

Cannot fetch logs

-

The logs for this task are inaccessible. Check the condition of the node the allocation is on.

+
+
+

Cannot fetch logs

+

The logs for this task are inaccessible. Check the condition of the node the allocation is on.

+
+
+ +
+
{{/if}}
diff --git a/ui/tests/integration/task-log-test.js b/ui/tests/integration/task-log-test.js index 67f66786eb7..bea9b342b72 100644 --- a/ui/tests/integration/task-log-test.js +++ b/ui/tests/integration/task-log-test.js @@ -190,6 +190,25 @@ module('Integration | Component | task log', function(hooks) { ); }); + test('Clicking stderr/stdout mode buttons does nothing when the mode remains the same', async function(assert) { + const { interval } = commonProps; + + run.later(() => { + click('[data-test-log-action="stdout"]'); + run.later(run, run.cancelTimers, interval * 6); + }, interval * 2); + + this.setProperties(commonProps); + await render(hbs`{{task-log allocation=allocation task=task}}`); + + await settled(); + assert.equal( + find('[data-test-log-cli]').textContent, + streamFrames[0] + streamFrames[0] + streamFrames[1], + 'Now includes second frame' + ); + }); + test('When the client is inaccessible, task-log falls back to requesting logs through the server', async function(assert) { run.later(run, run.cancelTimers, allowedConnectionTime * 2); @@ -219,6 +238,11 @@ module('Integration | Component | task log', function(hooks) { this.server.handledRequests.filter(req => req.url.startsWith(serverUrl)).length, 'Log request was later made to the server' ); + + assert.ok( + this.server.handledRequests.filter(req => clientUrlRegex.test(req.url))[0].aborted, + 'Client log request was aborted' + ); }); test('When both the client and the server are inaccessible, an error message is shown', async function(assert) { @@ -255,5 +279,53 @@ module('Integration | Component | task log', function(hooks) { 'Log request was later made to the server' ); assert.ok(find('[data-test-connection-error]'), 'An error message is shown'); + + await click('[data-test-connection-error-dismiss]'); + assert.notOk(find('[data-test-connection-error]'), 'The error message is dismissable'); + }); + + test('When the client is inaccessible, the server is accessible, and stderr is pressed before the client timeout occurs, the no connection error is not shown', async function(assert) { + // override client response to timeout + this.server.get( + `http://${HOST}/v1/client/fs/logs/:allocation_id`, + () => [400, {}, ''], + allowedConnectionTime * 2 + ); + + // Click stderr before the client request responds + run.later(() => { + click('[data-test-log-action="stderr"]'); + run.later(run, run.cancelTimers, commonProps.interval * 5); + }, allowedConnectionTime / 2); + + this.setProperties(commonProps); + await render(hbs`{{task-log + allocation=allocation + task=task + clientTimeout=clientTimeout + serverTimeout=serverTimeout}}`); + + await settled(); + + const clientUrlRegex = new RegExp(`${HOST}/v1/client/fs/logs/${commonProps.allocation.id}`); + const clientRequests = this.server.handledRequests.filter(req => clientUrlRegex.test(req.url)); + assert.ok( + clientRequests.find(req => req.queryParams.type === 'stdout'), + 'Client request for stdout' + ); + assert.ok( + clientRequests.find(req => req.queryParams.type === 'stderr'), + 'Client request for stderr' + ); + + const serverUrl = `/v1/client/fs/logs/${commonProps.allocation.id}`; + assert.ok( + this.server.handledRequests + .filter(req => req.url.startsWith(serverUrl)) + .find(req => req.queryParams.type === 'stderr'), + 'Server request for stderr' + ); + + assert.notOk(find('[data-test-connection-error]'), 'An error message is not shown'); }); });