From 7ece3a7d77684ea0d1b897c52b953720c5b11fe6 Mon Sep 17 00:00:00 2001 From: Alex Dadgar Date: Thu, 24 May 2018 15:54:28 -0700 Subject: [PATCH 01/27] Force closing of pipe to child process --- client/driver/executor/executor.go | 85 +++++++++++++++++++++++------- 1 file changed, 66 insertions(+), 19 deletions(-) diff --git a/client/driver/executor/executor.go b/client/driver/executor/executor.go index 6b18612f8..ba1a72c9f 100644 --- a/client/driver/executor/executor.go +++ b/client/driver/executor/executor.go @@ -162,8 +162,8 @@ type UniversalExecutor struct { processExited chan interface{} fsIsolationEnforced bool - lre *logging.FileRotator - lro *logging.FileRotator + lre *logRotatorWrapper + lro *logRotatorWrapper rotatorLock sync.Mutex syslogServer *logging.SyslogServer @@ -252,8 +252,8 @@ func (e *UniversalExecutor) LaunchCmd(command *ExecCommand) (*ProcessState, erro if err := e.configureLoggers(); err != nil { return nil, err } - e.cmd.Stdout = e.lro - e.cmd.Stderr = e.lre + e.cmd.Stdout = e.lro.processOutWriter + e.cmd.Stderr = e.lre.processOutWriter // Look up the binary path and make it executable absPath, err := e.lookupBin(e.ctx.TaskEnv.ReplaceEnv(command.Cmd)) @@ -348,7 +348,18 @@ func (e *UniversalExecutor) configureLoggers() error { if err != nil { return fmt.Errorf("error creating new stdout log file for %q: %v", e.ctx.Task.Name, err) } - e.lro = lro + + r, w, err := os.Pipe() + if err != nil { + return fmt.Errorf("failed to create os.Pipe for extracting logs: %v", err) + } + + e.lro = &logRotatorWrapper{ + processOutWriter: w, + processOutReader: r, + rotatorWriter: lro, + } + e.lro.Start() } if e.lre == nil { @@ -357,7 +368,18 @@ func (e *UniversalExecutor) configureLoggers() error { if err != nil { return fmt.Errorf("error creating new stderr log file for %q: %v", e.ctx.Task.Name, err) } - e.lre = lre + + r, w, err := os.Pipe() + if err != nil { + return fmt.Errorf("failed to create os.Pipe for extracting logs: %v", err) + } + + e.lre = &logRotatorWrapper{ + processOutWriter: w, + processOutReader: r, + rotatorWriter: lre, + } + e.lre.Start() } return nil } @@ -375,14 +397,14 @@ func (e *UniversalExecutor) UpdateLogConfig(logConfig *structs.LogConfig) error if e.lro == nil { return fmt.Errorf("log rotator for stdout doesn't exist") } - e.lro.MaxFiles = logConfig.MaxFiles - e.lro.FileSize = int64(logConfig.MaxFileSizeMB * 1024 * 1024) + e.lro.rotatorWriter.MaxFiles = logConfig.MaxFiles + e.lro.rotatorWriter.FileSize = int64(logConfig.MaxFileSizeMB * 1024 * 1024) if e.lre == nil { return fmt.Errorf("log rotator for stderr doesn't exist") } - e.lre.MaxFiles = logConfig.MaxFiles - e.lre.FileSize = int64(logConfig.MaxFileSizeMB * 1024 * 1024) + e.lre.rotatorWriter.MaxFiles = logConfig.MaxFiles + e.lre.rotatorWriter.FileSize = int64(logConfig.MaxFileSizeMB * 1024 * 1024) return nil } @@ -393,10 +415,10 @@ func (e *UniversalExecutor) UpdateTask(task *structs.Task) error { e.rotatorLock.Lock() if e.lro != nil && e.lre != nil { fileSize := int64(task.LogConfig.MaxFileSizeMB * 1024 * 1024) - e.lro.MaxFiles = task.LogConfig.MaxFiles - e.lro.FileSize = fileSize - e.lre.MaxFiles = task.LogConfig.MaxFiles - e.lre.FileSize = fileSize + e.lro.rotatorWriter.MaxFiles = task.LogConfig.MaxFiles + e.lro.rotatorWriter.FileSize = fileSize + e.lre.rotatorWriter.MaxFiles = task.LogConfig.MaxFiles + e.lre.rotatorWriter.FileSize = fileSize } e.rotatorLock.Unlock() return nil @@ -799,7 +821,7 @@ func (e *UniversalExecutor) LaunchSyslogServer() (*SyslogServerState, error) { e.syslogServer = logging.NewSyslogServer(l, e.syslogChan, e.logger) go e.syslogServer.Start() - go e.collectLogs(e.lre, e.lro) + go e.collectLogs(e.lre.rotatorWriter, e.lro.rotatorWriter) syslogAddr := fmt.Sprintf("%s://%s", l.Addr().Network(), l.Addr().String()) return &SyslogServerState{Addr: syslogAddr}, nil } @@ -809,11 +831,36 @@ func (e *UniversalExecutor) collectLogs(we io.Writer, wo io.Writer) { // If the severity of the log line is err then we write to stderr // otherwise all messages go to stdout if logParts.Severity == syslog.LOG_ERR { - e.lre.Write(logParts.Message) - e.lre.Write([]byte{'\n'}) + we.Write(logParts.Message) + we.Write([]byte{'\n'}) } else { - e.lro.Write(logParts.Message) - e.lro.Write([]byte{'\n'}) + wo.Write(logParts.Message) + wo.Write([]byte{'\n'}) } } } + +// logRotatorWrapper wraps our log rotator and exposes a pipe that can feed the +// log rotator data. The processOutWriter should be attached to the process and +// Start is called to copy data from the reader to the rotator. +type logRotatorWrapper struct { + processOutWriter *os.File + processOutReader *os.File + rotatorWriter *logging.FileRotator +} + +// Start starts a go-routine that copies from the pipe into the rotator. +func (l *logRotatorWrapper) Start() { + go func() { + io.Copy(l.rotatorWriter, l.processOutReader) + l.processOutReader.Close() // in case io.Copy stopped due to write error + }() + return +} + +// Close closes the rotator and the process writer to ensure that the Wait +// command exits. +func (l *logRotatorWrapper) Close() error { + l.rotatorWriter.Close() + return l.processOutWriter.Close() +} From 52bed806fca8954ef3cec0aa27a9e468299cbc83 Mon Sep 17 00:00:00 2001 From: Alex Dadgar Date: Thu, 24 May 2018 16:25:20 -0700 Subject: [PATCH 02/27] cleanup --- client/driver/executor/executor.go | 48 +++++++++++++++++------------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/client/driver/executor/executor.go b/client/driver/executor/executor.go index ba1a72c9f..7ddccce69 100644 --- a/client/driver/executor/executor.go +++ b/client/driver/executor/executor.go @@ -349,17 +349,11 @@ func (e *UniversalExecutor) configureLoggers() error { return fmt.Errorf("error creating new stdout log file for %q: %v", e.ctx.Task.Name, err) } - r, w, err := os.Pipe() + r, err := NewLogRotatorWrapper(lro) if err != nil { - return fmt.Errorf("failed to create os.Pipe for extracting logs: %v", err) + return err } - - e.lro = &logRotatorWrapper{ - processOutWriter: w, - processOutReader: r, - rotatorWriter: lro, - } - e.lro.Start() + e.lro = r } if e.lre == nil { @@ -369,17 +363,11 @@ func (e *UniversalExecutor) configureLoggers() error { return fmt.Errorf("error creating new stderr log file for %q: %v", e.ctx.Task.Name, err) } - r, w, err := os.Pipe() + r, err := NewLogRotatorWrapper(lre) if err != nil { - return fmt.Errorf("failed to create os.Pipe for extracting logs: %v", err) + return err } - - e.lre = &logRotatorWrapper{ - processOutWriter: w, - processOutReader: r, - rotatorWriter: lre, - } - e.lre.Start() + e.lre = r } return nil } @@ -842,15 +830,33 @@ func (e *UniversalExecutor) collectLogs(we io.Writer, wo io.Writer) { // logRotatorWrapper wraps our log rotator and exposes a pipe that can feed the // log rotator data. The processOutWriter should be attached to the process and -// Start is called to copy data from the reader to the rotator. +// data will be copied from the reader to the rotator. type logRotatorWrapper struct { processOutWriter *os.File processOutReader *os.File rotatorWriter *logging.FileRotator } -// Start starts a go-routine that copies from the pipe into the rotator. -func (l *logRotatorWrapper) Start() { +// NewLogRotatorWrapper takes a rotator and returns a wrapper that has the +// processOutWriter to attach to the processes stdout or stderr. +func NewLogRotatorWrapper(rotator *logging.FileRotator) (*logRotatorWrapper, error) { + r, w, err := os.Pipe() + if err != nil { + return nil, fmt.Errorf("failed to create os.Pipe for extracting logs: %v", err) + } + + wrap := &logRotatorWrapper{ + processOutWriter: w, + processOutReader: r, + rotatorWriter: rotator, + } + wrap.start() + return wrap, nil +} + +// start starts a go-routine that copies from the pipe into the rotator. This is +// called by the constructor and not the user of the wrapper. +func (l *logRotatorWrapper) start() { go func() { io.Copy(l.rotatorWriter, l.processOutReader) l.processOutReader.Close() // in case io.Copy stopped due to write error From 73aa3aa9570590003ee33b68570a9265c7bafc6a Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 22 May 2018 14:48:27 -0700 Subject: [PATCH 03/27] XHR keys need to include the method as well The URL alone doesn't guarantee uniqueness --- ui/app/adapters/job.js | 4 ++-- ui/app/adapters/watchable.js | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ui/app/adapters/job.js b/ui/app/adapters/job.js index 984564572..0e16fdcb4 100644 --- a/ui/app/adapters/job.js +++ b/ui/app/adapters/job.js @@ -49,9 +49,9 @@ export default Watchable.extend({ xhrKey(url, method, options = {}) { const namespace = options.data && options.data.namespace; if (namespace) { - return `${url}?namespace=${namespace}`; + return `${method} ${url}?namespace=${namespace}`; } - return url; + return `${method} ${url}`; }, relationshipFallbackLinks: { diff --git a/ui/app/adapters/watchable.js b/ui/app/adapters/watchable.js index 2f3257efd..9f7c072cc 100644 --- a/ui/app/adapters/watchable.js +++ b/ui/app/adapters/watchable.js @@ -51,8 +51,8 @@ export default ApplicationAdapter.extend({ return ajaxOptions; }, - xhrKey(url /* method, options */) { - return url; + xhrKey(url, method /* options */) { + return `${method} ${url}`; }, findAll(store, type, sinceToken, snapshotRecordArray, additionalParams = {}) { @@ -149,7 +149,7 @@ export default ApplicationAdapter.extend({ return; } const url = this.urlForFindRecord(id, modelName); - this.get('xhrs').cancel(url); + this.get('xhrs').cancel(`GET ${url}`); }, cancelFindAll(modelName) { @@ -161,7 +161,7 @@ export default ApplicationAdapter.extend({ if (params) { url = `${url}?${params}`; } - this.get('xhrs').cancel(url); + this.get('xhrs').cancel(`GET ${url}`); }, cancelReloadRelationship(model, relationshipName) { @@ -175,7 +175,7 @@ export default ApplicationAdapter.extend({ ); } else { const url = model[relationship.kind](relationship.key).link(); - this.get('xhrs').cancel(url); + this.get('xhrs').cancel(`GET ${url}`); } }, }); From f160f70f044f148b21cae77c03bbb9c2cebf48fd Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 25 May 2018 09:13:18 -0700 Subject: [PATCH 04/27] Add a test to assert that canceling GETs can't instead cancel DELETEs --- ui/tests/unit/adapters/job-test.js | 37 +++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/ui/tests/unit/adapters/job-test.js b/ui/tests/unit/adapters/job-test.js index b454cc8b1..aa6827d5a 100644 --- a/ui/tests/unit/adapters/job-test.js +++ b/ui/tests/unit/adapters/job-test.js @@ -1,3 +1,4 @@ +import EmberObject from '@ember/object'; import { run } from '@ember/runloop'; import { assign } from '@ember/polyfills'; import { test } from 'ember-qunit'; @@ -10,8 +11,12 @@ moduleForAdapter('job', 'Unit | Adapter | Job', { 'adapter:job', 'service:token', 'service:system', - 'model:namespace', + 'model:allocation', + 'model:deployment', + 'model:evaluation', 'model:job-summary', + 'model:job-version', + 'model:namespace', 'adapter:application', 'service:watchList', ], @@ -292,6 +297,36 @@ test('requests can be canceled even if multiple requests for the same URL were m }); }); +test('canceling a find record request will never cancel a request with the same url but different method', function(assert) { + const { pretender } = this.server; + const jobId = JSON.stringify(['job-1', 'default']); + + pretender.get('/v1/job/:id', () => [200, {}, '{}'], true); + pretender.delete('/v1/job/:id', () => [204, {}, ''], 200); + + this.subject().findRecord(null, { modelName: 'job' }, jobId, { + reload: true, + adapterOptions: { watch: true }, + }); + + this.subject().stop(EmberObject.create({ id: jobId })); + + const { request: getXHR } = pretender.requestReferences[0]; + const { request: deleteXHR } = pretender.requestReferences[1]; + assert.equal(getXHR.status, 0, 'Get request is still pending'); + assert.equal(deleteXHR.status, 0, 'Delete request is still pending'); + + // Schedule the cancelation before waiting + run.next(() => { + this.subject().cancelFindRecord('job', jobId); + }); + + return wait().then(() => { + assert.ok(getXHR.aborted, 'Get request was aborted'); + assert.notOk(deleteXHR.aborted, 'Delete request was aborted'); + }); +}); + function makeMockModel(id, options) { return assign( { From e8bbd0fb0bea47732f1ae1f4f59a8ff5d8eec0cf Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 25 May 2018 09:15:45 -0700 Subject: [PATCH 05/27] Refactor the job xhrKey to use super --- ui/app/adapters/job.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ui/app/adapters/job.js b/ui/app/adapters/job.js index 0e16fdcb4..eeba06522 100644 --- a/ui/app/adapters/job.js +++ b/ui/app/adapters/job.js @@ -47,11 +47,12 @@ export default Watchable.extend({ }, xhrKey(url, method, options = {}) { + const plainKey = this._super(...arguments); const namespace = options.data && options.data.namespace; if (namespace) { - return `${method} ${url}?namespace=${namespace}`; + return `${plainKey}?namespace=${namespace}`; } - return `${method} ${url}`; + return plainKey; }, relationshipFallbackLinks: { From a7eca1686424b15f76f3ff0ec50e80c57e2516fd Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Mon, 7 May 2018 16:53:23 -0700 Subject: [PATCH 06/27] Remove stale dev code This was used to get around direct requests to clients. The UI will now automatically route through the server. --- ui/app/serializers/node.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/ui/app/serializers/node.js b/ui/app/serializers/node.js index e0ecfc9a7..78bc31f42 100644 --- a/ui/app/serializers/node.js +++ b/ui/app/serializers/node.js @@ -9,12 +9,6 @@ export default ApplicationSerializer.extend({ }, normalize(modelClass, hash) { - // Proxy local agent to the same proxy express server Ember is using - // to avoid CORS - if (this.get('config.isDev') && hash.HTTPAddr === '127.0.0.1:4646') { - hash.HTTPAddr = '127.0.0.1:4200'; - } - return this._super(modelClass, hash); }, From bc6467c6aedfde4969d0e3c59d77e176c413bf5f Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 8 May 2018 09:39:27 -0700 Subject: [PATCH 07/27] Data modeling for node events and node drivers --- ui/app/models/node-driver.js | 15 +++++++++++++++ ui/app/models/node-event.js | 12 ++++++++++++ ui/app/models/node.js | 5 ++++- ui/app/serializers/node-event.js | 7 +++++++ ui/app/serializers/node.js | 7 +++++++ 5 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 ui/app/models/node-driver.js create mode 100644 ui/app/models/node-event.js create mode 100644 ui/app/serializers/node-event.js diff --git a/ui/app/models/node-driver.js b/ui/app/models/node-driver.js new file mode 100644 index 000000000..c656d5281 --- /dev/null +++ b/ui/app/models/node-driver.js @@ -0,0 +1,15 @@ +import Fragment from 'ember-data-model-fragments/fragment'; +import attr from 'ember-data/attr'; +import { fragmentOwner } from 'ember-data-model-fragments/attributes'; +import { fragment } from 'ember-data-model-fragments/attributes'; + +export default Fragment.extend({ + node: fragmentOwner(), + + attributes: fragment('node-attributes'), + name: attr('string'), + detected: attr('boolean', { defaultValue: false }), + healthy: attr('boolean', { defaultValue: false }), + healthDescription: attr('string'), + updateTime: attr('date'), +}); diff --git a/ui/app/models/node-event.js b/ui/app/models/node-event.js new file mode 100644 index 000000000..2e6ee30d5 --- /dev/null +++ b/ui/app/models/node-event.js @@ -0,0 +1,12 @@ +import Fragment from 'ember-data-model-fragments/fragment'; +import attr from 'ember-data/attr'; +import { fragmentOwner } from 'ember-data-model-fragments/attributes'; + +export default Fragment.extend({ + node: fragmentOwner(), + + message: attr('string'), + subsystem: attr('string'), + details: attr(), + time: attr('date'), +}); diff --git a/ui/app/models/node.js b/ui/app/models/node.js index 4b5f34a44..b41613106 100644 --- a/ui/app/models/node.js +++ b/ui/app/models/node.js @@ -2,7 +2,7 @@ import { computed } from '@ember/object'; import Model from 'ember-data/model'; import attr from 'ember-data/attr'; import { hasMany } from 'ember-data/relationships'; -import { fragment } from 'ember-data-model-fragments/attributes'; +import { fragment, fragmentArray } from 'ember-data-model-fragments/attributes'; import shortUUIDProperty from '../utils/properties/short-uuid'; import ipParts from '../utils/ip-parts'; @@ -37,4 +37,7 @@ export default Model.extend({ }), allocations: hasMany('allocations', { inverse: 'node' }), + + drivers: fragmentArray('node-driver'), + events: fragmentArray('node-event'), }); diff --git a/ui/app/serializers/node-event.js b/ui/app/serializers/node-event.js new file mode 100644 index 000000000..606c6dadd --- /dev/null +++ b/ui/app/serializers/node-event.js @@ -0,0 +1,7 @@ +import ApplicationSerializer from './application'; + +export default ApplicationSerializer.extend({ + attrs: { + time: 'Timestamp', + }, +}); diff --git a/ui/app/serializers/node.js b/ui/app/serializers/node.js index 78bc31f42..a8d3410e2 100644 --- a/ui/app/serializers/node.js +++ b/ui/app/serializers/node.js @@ -1,3 +1,5 @@ +import { get } from '@ember/object'; +import { assign } from '@ember/polyfills'; import { inject as service } from '@ember/service'; import ApplicationSerializer from './application'; @@ -9,6 +11,11 @@ export default ApplicationSerializer.extend({ }, normalize(modelClass, hash) { + // Transform the map-based Drivers object into an array-based NodeDriver fragment list + hash.Drivers = Object.keys(get(hash, 'Drivers') || {}).map(key => { + return assign({}, get(hash, `Drivers.${key}`), { Name: key }); + }); + return this._super(modelClass, hash); }, From d2810bbaf4145e4d0e242902a59d377aaaf143a1 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 8 May 2018 11:22:36 -0700 Subject: [PATCH 08/27] Show driver summary on the client detail page --- ui/app/models/node.js | 8 ++++++++ ui/app/templates/clients/client.hbs | 24 +++++++++++++++++++++--- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/ui/app/models/node.js b/ui/app/models/node.js index b41613106..40c68e376 100644 --- a/ui/app/models/node.js +++ b/ui/app/models/node.js @@ -40,4 +40,12 @@ export default Model.extend({ drivers: fragmentArray('node-driver'), events: fragmentArray('node-event'), + + detectedDrivers: computed('drivers.@each.detected', function() { + return this.get('drivers').filterBy('detected'); + }), + + unhealthyDrivers: computed('detectedDrivers.@each.healthy', function() { + return this.get('detectedDrivers').filterBy('healthy', false); + }), }); diff --git a/ui/app/templates/clients/client.hbs b/ui/app/templates/clients/client.hbs index b9ec625aa..57f18e3f1 100644 --- a/ui/app/templates/clients/client.hbs +++ b/ui/app/templates/clients/client.hbs @@ -17,9 +17,27 @@
Client Details - Status {{model.status}} - Address {{model.httpAddr}} - Datacenter {{model.datacenter}} + + Status + {{model.status}} + + + Address + {{model.httpAddr}} + + + Datacenter + {{model.datacenter}} + + + Drivers + {{#if model.unhealthyDrivers.length}} + {{x-icon "warning" class="is-text is-warning"}} + {{model.unhealthyDrivers.length}} of {{model.detectedDrivers.length}} {{pluralize "driver" model.detectedDrivers.length}} unhealthy + {{else}} + All healthy + {{/if}} +
From 76caeb508308a27828625dcfbc1483fc714d93f3 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 8 May 2018 11:23:42 -0700 Subject: [PATCH 09/27] Add a node events section to the node detail page --- ui/app/controllers/clients/client.js | 6 ++++++ ui/app/templates/clients/client.hbs | 28 ++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/ui/app/controllers/clients/client.js b/ui/app/controllers/clients/client.js index 008169f5e..6e2000116 100644 --- a/ui/app/controllers/clients/client.js +++ b/ui/app/controllers/clients/client.js @@ -24,6 +24,12 @@ export default Controller.extend(Sortable, Searchable, { listToSearch: alias('listSorted'), sortedAllocations: alias('listSearched'), + sortedEvents: computed('model.events.@each.time', function() { + return this.get('model.events') + .sortBy('time') + .reverse(); + }), + actions: { gotoAllocation(allocation) { this.transitionToRoute('allocations.allocation', allocation); diff --git a/ui/app/templates/clients/client.hbs b/ui/app/templates/clients/client.hbs index 57f18e3f1..5a9ee7f78 100644 --- a/ui/app/templates/clients/client.hbs +++ b/ui/app/templates/clients/client.hbs @@ -93,6 +93,34 @@ +
+
+ Client Events +
+
+ {{#list-table source=sortedEvents class="is-striped" as |t|}} + {{#t.head}} + Time + Subsystem + Message + {{/t.head}} + {{#t.body as |row|}} + + {{moment-format row.model.time "MM/DD/YY HH:mm:ss"}} + {{row.model.subsystem}} + + {{#if row.model.message}} + {{row.model.message}} + {{else}} + No message + {{/if}} + + + {{/t.body}} + {{/list-table}} +
+
+
Attributes From a022b618f4c74fa258cbc4d0da862fca7c01740f Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 8 May 2018 11:24:11 -0700 Subject: [PATCH 10/27] Show a warning icon on client node rows that have unhealthy drivers --- ui/app/templates/clients/index.hbs | 1 + ui/app/templates/components/client-node-row.hbs | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/ui/app/templates/clients/index.hbs b/ui/app/templates/clients/index.hbs index 1b43c78c3..888c05634 100644 --- a/ui/app/templates/clients/index.hbs +++ b/ui/app/templates/clients/index.hbs @@ -23,6 +23,7 @@ sortDescending=sortDescending class="with-foot" as |t|}} {{#t.head}} + {{#t.sort-by prop="id"}}ID{{/t.sort-by}} {{#t.sort-by class="is-200px is-truncatable" prop="name"}}Name{{/t.sort-by}} {{#t.sort-by prop="status"}}Status{{/t.sort-by}} diff --git a/ui/app/templates/components/client-node-row.hbs b/ui/app/templates/components/client-node-row.hbs index 184a00e2b..9a946006d 100644 --- a/ui/app/templates/components/client-node-row.hbs +++ b/ui/app/templates/components/client-node-row.hbs @@ -1,3 +1,10 @@ + + {{#if node.unhealthyDrivers.length}} + + {{x-icon "warning" class="is-warning"}} + + {{/if}} + {{#link-to "clients.client" node.id class="is-primary"}}{{node.shortId}}{{/link-to}} {{node.name}} {{node.status}} From 53244e24624c76a3d1f89062438a1244236ab876 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 8 May 2018 18:08:30 -0700 Subject: [PATCH 11/27] New accordion component Follows the same style as the table and pagination components. --- ui/app/components/list-accordion.js | 30 +++++++++++++ .../list-accordion/accordion-body.js | 6 +++ .../list-accordion/accordion-head.js | 14 +++++++ ui/app/styles/components.scss | 1 + ui/app/styles/components/accordion.scss | 42 +++++++++++++++++++ .../templates/components/list-accordion.hbs | 10 +++++ .../list-accordion/accordion-body.hbs | 5 +++ .../list-accordion/accordion-head.hbs | 8 ++++ 8 files changed, 116 insertions(+) create mode 100644 ui/app/components/list-accordion.js create mode 100644 ui/app/components/list-accordion/accordion-body.js create mode 100644 ui/app/components/list-accordion/accordion-head.js create mode 100644 ui/app/styles/components/accordion.scss create mode 100644 ui/app/templates/components/list-accordion.hbs create mode 100644 ui/app/templates/components/list-accordion/accordion-body.hbs create mode 100644 ui/app/templates/components/list-accordion/accordion-head.hbs diff --git a/ui/app/components/list-accordion.js b/ui/app/components/list-accordion.js new file mode 100644 index 000000000..9e004645d --- /dev/null +++ b/ui/app/components/list-accordion.js @@ -0,0 +1,30 @@ +import Component from '@ember/component'; +import { computed, get } from '@ember/object'; + +export default Component.extend({ + classNames: ['accordion'], + + key: 'id', + source: computed(() => []), + + decoratedSource: computed('source.[]', function() { + const stateCache = this.get('stateCache'); + const key = this.get('key'); + const deepKey = `item.${key}`; + + const decoratedSource = this.get('source').map(item => { + const cacheItem = stateCache.findBy(deepKey, get(item, key)); + return { + item, + isOpen: cacheItem ? !!cacheItem.isOpen : false, + }; + }); + + this.set('stateCache', decoratedSource); + return decoratedSource; + }), + + // When source updates come in, the state cache is used to preserve + // open/close state. + stateCache: computed(() => []), +}); diff --git a/ui/app/components/list-accordion/accordion-body.js b/ui/app/components/list-accordion/accordion-body.js new file mode 100644 index 000000000..32397fce6 --- /dev/null +++ b/ui/app/components/list-accordion/accordion-body.js @@ -0,0 +1,6 @@ +import Component from '@ember/component'; + +export default Component.extend({ + tagName: '', + isOpen: false, +}); diff --git a/ui/app/components/list-accordion/accordion-head.js b/ui/app/components/list-accordion/accordion-head.js new file mode 100644 index 000000000..4de6d4edc --- /dev/null +++ b/ui/app/components/list-accordion/accordion-head.js @@ -0,0 +1,14 @@ +import Component from '@ember/component'; + +export default Component.extend({ + classNames: ['accordion-head'], + classNameBindings: ['isOpen::is-light', 'isExpandable::is-inactive'], + + buttonLabel: 'toggle', + isOpen: false, + isExpandable: true, + item: null, + + onClose() {}, + onOpen() {}, +}); diff --git a/ui/app/styles/components.scss b/ui/app/styles/components.scss index 49b513806..83efbb676 100644 --- a/ui/app/styles/components.scss +++ b/ui/app/styles/components.scss @@ -1,3 +1,4 @@ +@import './components/accordion'; @import './components/badge'; @import './components/boxed-section'; @import './components/cli-window'; diff --git a/ui/app/styles/components/accordion.scss b/ui/app/styles/components/accordion.scss new file mode 100644 index 000000000..17ca8bcc5 --- /dev/null +++ b/ui/app/styles/components/accordion.scss @@ -0,0 +1,42 @@ +.accordion { + .accordion-head, + .accordion-body { + border: 1px solid $grey-blue; + border-bottom: none; + padding: 0.75em 1.5em; + + &:first-child { + border-top-left-radius: $radius; + border-top-right-radius: $radius; + } + + &:last-child { + border-bottom: 1px solid $grey-blue; + border-bottom-left-radius: $radius; + border-bottom-right-radius: $radius; + } + } + + .accordion-head { + display: flex; + background: $white-ter; + flex: 1; + + &.is-light { + background: $white; + } + + &.is-inactive { + color: $grey-light; + } + + .accordion-head-content { + width: 100%; + } + + .accordion-toggle { + flex-basis: 0; + white-space: nowrap; + } + } +} diff --git a/ui/app/templates/components/list-accordion.hbs b/ui/app/templates/components/list-accordion.hbs new file mode 100644 index 000000000..67678cef2 --- /dev/null +++ b/ui/app/templates/components/list-accordion.hbs @@ -0,0 +1,10 @@ +{{#each decoratedSource as |item|}} + {{yield (hash + head=(component "list-accordion/accordion-head" + isOpen=item.isOpen + onOpen=(action (mut item.isOpen) true) + onClose=(action (mut item.isOpen) false)) + body=(component "list-accordion/accordion-body" isOpen=item.isOpen) + item=item.item + )}} +{{/each}} diff --git a/ui/app/templates/components/list-accordion/accordion-body.hbs b/ui/app/templates/components/list-accordion/accordion-body.hbs new file mode 100644 index 000000000..894e66818 --- /dev/null +++ b/ui/app/templates/components/list-accordion/accordion-body.hbs @@ -0,0 +1,5 @@ +{{#if isOpen}} +
+ {{yield}} +
+{{/if}} diff --git a/ui/app/templates/components/list-accordion/accordion-head.hbs b/ui/app/templates/components/list-accordion/accordion-head.hbs new file mode 100644 index 000000000..a362b1165 --- /dev/null +++ b/ui/app/templates/components/list-accordion/accordion-head.hbs @@ -0,0 +1,8 @@ +
+ {{yield}} +
+ From 8fa044a52a79ac99e895f000be3f89c2c6bac6a3 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 8 May 2018 18:09:20 -0700 Subject: [PATCH 12/27] Add driver status accordion section to the client detail page --- ui/app/controllers/clients/client.js | 4 ++ ui/app/models/node-driver.js | 5 +++ ui/app/templates/clients/client.hbs | 58 ++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+) diff --git a/ui/app/controllers/clients/client.js b/ui/app/controllers/clients/client.js index 6e2000116..26dfc9b64 100644 --- a/ui/app/controllers/clients/client.js +++ b/ui/app/controllers/clients/client.js @@ -30,6 +30,10 @@ export default Controller.extend(Sortable, Searchable, { .reverse(); }), + sortedDrivers: computed('model.drivers.@each.name', function() { + return this.get('model.drivers').sortBy('name'); + }), + actions: { gotoAllocation(allocation) { this.transitionToRoute('allocations.allocation', allocation); diff --git a/ui/app/models/node-driver.js b/ui/app/models/node-driver.js index c656d5281..d412867e7 100644 --- a/ui/app/models/node-driver.js +++ b/ui/app/models/node-driver.js @@ -1,4 +1,5 @@ import Fragment from 'ember-data-model-fragments/fragment'; +import { computed } from '@ember/object'; import attr from 'ember-data/attr'; import { fragmentOwner } from 'ember-data-model-fragments/attributes'; import { fragment } from 'ember-data-model-fragments/attributes'; @@ -12,4 +13,8 @@ export default Fragment.extend({ healthy: attr('boolean', { defaultValue: false }), healthDescription: attr('string'), updateTime: attr('date'), + + healthClass: computed('healthy', function() { + return this.get('healthy') ? 'running' : 'failed'; + }), }); diff --git a/ui/app/templates/clients/client.hbs b/ui/app/templates/clients/client.hbs index 5a9ee7f78..79a18e34c 100644 --- a/ui/app/templates/clients/client.hbs +++ b/ui/app/templates/clients/client.hbs @@ -121,6 +121,64 @@
+
+
+ Driver Status +
+
+ {{#list-accordion source=sortedDrivers key="name" as |a|}} + {{#a.head buttonLabel="details" isExpandable=a.item.detected}} +
+
+ {{a.item.name}} +
+
+ {{#if a.item.detected}} + + + {{if a.item.healthy "Healthy" "Unhealthy"}} + + {{/if}} +
+
+ + Detected + {{if a.item.detected "Yes" "No"}} + + + + Last Updated + {{moment-from-now a.item.updateTime interval=1000}} + + +
+
+ {{/a.head}} + {{#a.body}} +

{{a.item.healthDescription}}

+
+
+ {{capitalize a.item.name}} Attributes +
+ {{#if a.item.attributes.attributesStructured}} +
+ {{attributes-table + attributes=a.item.attributes.attributesStructured + class="attributes-table"}} +
+ {{else}} +
+
+

No Driver Attributes

+
+
+ {{/if}} +
+ {{/a.body}} + {{/list-accordion}} +
+
+
Attributes From 1a55f18085b7358d098a52500dcd80d5e4619e9c Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 9 May 2018 17:04:45 -0700 Subject: [PATCH 13/27] Spread the driver health love throughout job models --- ui/app/models/allocation.js | 12 ++++++++++++ ui/app/models/job.js | 22 ++++++++++++++++++++++ ui/app/models/node.js | 4 ++++ ui/app/models/task-group.js | 6 ++++++ ui/app/models/task-state.js | 10 ++++++++++ ui/package.json | 1 + ui/yarn.lock | 4 ++++ 7 files changed, 59 insertions(+) diff --git a/ui/app/models/allocation.js b/ui/app/models/allocation.js index 048e31e55..f4dbad07d 100644 --- a/ui/app/models/allocation.js +++ b/ui/app/models/allocation.js @@ -4,6 +4,7 @@ import Model from 'ember-data/model'; import attr from 'ember-data/attr'; import { belongsTo } from 'ember-data/relationships'; import { fragment, fragmentArray } from 'ember-data-model-fragments/attributes'; +import intersection from 'npm:lodash.intersection'; import shortUUIDProperty from '../utils/properties/short-uuid'; import AllocationStats from '../utils/classes/allocation-stats'; @@ -61,6 +62,17 @@ export default Model.extend({ return taskGroups && taskGroups.findBy('name', this.get('taskGroupName')); }), + unhealthyDrivers: computed('taskGroup.drivers.[]', 'node.unhealthyDriverNames.[]', function() { + const taskGroupUnhealthyDrivers = this.get('taskGroup.drivers'); + const nodeUnhealthyDrivers = this.get('node.unhealthyDriverNames'); + + if (taskGroupUnhealthyDrivers && nodeUnhealthyDrivers) { + return intersection(taskGroupUnhealthyDrivers, nodeUnhealthyDrivers); + } + + return []; + }), + fetchStats() { return this.get('token') .authorizedRequest(`/v1/client/allocation/${this.get('id')}/stats`) diff --git a/ui/app/models/job.js b/ui/app/models/job.js index b8ba9670f..621b869ca 100644 --- a/ui/app/models/job.js +++ b/ui/app/models/job.js @@ -116,6 +116,28 @@ export default Model.extend({ evaluations: hasMany('evaluations'), namespace: belongsTo('namespace'), + drivers: computed('taskGroups.@each.drivers', function() { + return this.get('taskGroups') + .mapBy('drivers') + .reduce((all, drivers) => { + all.push(...drivers); + return all; + }, []) + .uniq(); + }), + + // Getting all unhealthy drivers for a job can be incredibly expensive if the job + // has many allocations. This can lead to making an API request for many nodes. + unhealthyDrivers: computed('allocations.@each.unhealthyDrivers.[]', function() { + return this.get('allocations') + .mapBy('unhealthyDrivers') + .reduce((all, drivers) => { + all.push(...drivers); + return all; + }, []) + .uniq(); + }), + hasBlockedEvaluation: computed('evaluations.@each.isBlocked', function() { return this.get('evaluations') .toArray() diff --git a/ui/app/models/node.js b/ui/app/models/node.js index 40c68e376..4dbb4fec6 100644 --- a/ui/app/models/node.js +++ b/ui/app/models/node.js @@ -48,4 +48,8 @@ export default Model.extend({ unhealthyDrivers: computed('detectedDrivers.@each.healthy', function() { return this.get('detectedDrivers').filterBy('healthy', false); }), + + unhealthyDriverNames: computed('unhealthyDrivers.@each.name', function() { + return this.get('unhealthyDrivers').mapBy('name'); + }), }); diff --git a/ui/app/models/task-group.js b/ui/app/models/task-group.js index 9be65bdba..ab1a37bbe 100644 --- a/ui/app/models/task-group.js +++ b/ui/app/models/task-group.js @@ -14,6 +14,12 @@ export default Fragment.extend({ tasks: fragmentArray('task'), + drivers: computed('tasks.@each.driver', function() { + return this.get('tasks') + .mapBy('driver') + .uniq(); + }), + allocations: computed('job.allocations.@each.taskGroup', function() { return maybe(this.get('job.allocations')).filterBy('taskGroupName', this.get('name')); }), diff --git a/ui/app/models/task-state.js b/ui/app/models/task-state.js index 217e87459..e0c8918c8 100644 --- a/ui/app/models/task-state.js +++ b/ui/app/models/task-state.js @@ -1,5 +1,6 @@ import { none } from '@ember/object/computed'; import { computed } from '@ember/object'; +import { alias } from '@ember/object/computed'; import Fragment from 'ember-data-model-fragments/fragment'; import attr from 'ember-data/attr'; import { fragment, fragmentOwner, fragmentArray } from 'ember-data-model-fragments/attributes'; @@ -19,6 +20,15 @@ export default Fragment.extend({ return tasks && tasks.findBy('name', this.get('name')); }), + driver: alias('task.driver'), + + // TaskState represents a task running on a node, so in addition to knowing the + // driver via the task, the health of the driver is also known via the node + driverStatus: computed('task.driver', 'allocation.node.drivers.[]', function() { + const nodeDrivers = this.get('allocation.node.drivers') || []; + return nodeDrivers.findBy('name', this.get('task.driver')); + }), + resources: fragment('resources'), events: fragmentArray('task-event'), diff --git a/ui/package.json b/ui/package.json index 649c495a3..566b798c9 100644 --- a/ui/package.json +++ b/ui/package.json @@ -72,6 +72,7 @@ "json-formatter-js": "^2.2.0", "lint-staged": "^6.0.0", "loader.js": "^4.2.3", + "lodash.intersection": "^4.4.0", "prettier": "^1.4.4", "query-string": "^5.0.0" }, diff --git a/ui/yarn.lock b/ui/yarn.lock index 6f921b680..795e0cf81 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -5691,6 +5691,10 @@ lodash.identity@~2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/lodash.identity/-/lodash.identity-2.3.0.tgz#6b01a210c9485355c2a913b48b6711219a173ded" +lodash.intersection@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.intersection/-/lodash.intersection-4.4.0.tgz#0a11ba631d0e95c23c7f2f4cbb9a692ed178e705" + lodash.isarguments@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" From d4ab1a23878cd3557a26cf41dd5881f1ad4f0784 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 9 May 2018 17:09:07 -0700 Subject: [PATCH 14/27] Show a warning on task rows on the alloc detail page The warning shows up when the task's driver is unhealthy on the node the task is running on. --- ui/app/templates/allocations/allocation/index.hbs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ui/app/templates/allocations/allocation/index.hbs b/ui/app/templates/allocations/allocation/index.hbs index 1aa22ab0a..b95afd44c 100644 --- a/ui/app/templates/allocations/allocation/index.hbs +++ b/ui/app/templates/allocations/allocation/index.hbs @@ -40,6 +40,7 @@ sortDescending=sortDescending class="is-striped" as |t|}} {{#t.head}} + {{#t.sort-by prop="name"}}Name{{/t.sort-by}} {{#t.sort-by prop="state"}}State{{/t.sort-by}} Last Event @@ -48,6 +49,13 @@ {{/t.head}} {{#t.body as |row|}} + + {{#if (not row.model.driverStatus.healthy)}} + + {{x-icon "warning" class="is-warning"}} + + {{/if}} + {{#link-to "allocations.allocation.task" row.model.allocation row.model}} {{row.model.name}} From 6c44b6da9da67747e2c4a8e223657a36acaea5f4 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 10 May 2018 16:28:18 -0700 Subject: [PATCH 15/27] Fix narrow table column padding --- ui/app/styles/core/table.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ui/app/styles/core/table.scss b/ui/app/styles/core/table.scss index 97ff41845..63822653b 100644 --- a/ui/app/styles/core/table.scss +++ b/ui/app/styles/core/table.scss @@ -86,6 +86,11 @@ &.is-narrow { padding: 1.25em 0 1.25em 0.5em; + + & + th, + & + td { + padding-left: 0.5em; + } } // Only use px modifiers when text needs to be truncated. From 7539ea739bb1722b5f8c6e231ea6ad2b708ffc56 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 11 May 2018 11:35:48 -0700 Subject: [PATCH 16/27] Add driver warning to allocation rows --- ui/app/templates/components/allocation-row.hbs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ui/app/templates/components/allocation-row.hbs b/ui/app/templates/components/allocation-row.hbs index b2f7e1103..8f8590702 100644 --- a/ui/app/templates/components/allocation-row.hbs +++ b/ui/app/templates/components/allocation-row.hbs @@ -1,4 +1,9 @@ + {{#if allocation.unhealthyDrivers.length}} + + {{x-icon "warning" class="is-warning"}} + + {{/if}} {{#if allocation.nextAllocation}} {{x-icon "history" class="is-faded"}} From c32738e774b15605564a71cefedea4399bdd9e2b Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 11 May 2018 17:30:15 -0700 Subject: [PATCH 17/27] NodeEvent and NodeDriver modeling in Mirage --- ui/mirage/factories/node-event.js | 12 ++++++++++ ui/mirage/factories/node.js | 40 +++++++++++++++++++++++++++++++ ui/mirage/factories/task-event.js | 2 +- ui/mirage/factories/task.js | 3 +++ ui/mirage/models/node.js | 5 ++++ ui/mirage/serializers/node.js | 6 +++++ 6 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 ui/mirage/factories/node-event.js create mode 100644 ui/mirage/models/node.js create mode 100644 ui/mirage/serializers/node.js diff --git a/ui/mirage/factories/node-event.js b/ui/mirage/factories/node-event.js new file mode 100644 index 000000000..a00435d56 --- /dev/null +++ b/ui/mirage/factories/node-event.js @@ -0,0 +1,12 @@ +import { Factory, faker } from 'ember-cli-mirage'; +import { provide } from '../utils'; + +const REF_TIME = new Date(); +const STATES = provide(10, faker.system.fileExt.bind(faker.system)); + +export default Factory.extend({ + subsystem: faker.list.random(...STATES), + message: () => faker.lorem.sentence(), + time: () => faker.date.past(2 / 365, REF_TIME), + details: null, +}); diff --git a/ui/mirage/factories/node.js b/ui/mirage/factories/node.js index ba4bd569e..3097ac19f 100644 --- a/ui/mirage/factories/node.js +++ b/ui/mirage/factories/node.js @@ -4,6 +4,7 @@ import { DATACENTERS, HOSTS } from '../common'; const UUIDS = provide(100, faker.random.uuid.bind(faker.random)); const NODE_STATUSES = ['initializing', 'ready', 'down']; +const REF_DATE = new Date(); export default Factory.extend({ id: i => (i / 100 >= 1 ? `${UUIDS[i]}-${i}` : UUIDS[i]), @@ -28,6 +29,8 @@ export default Factory.extend({ }, }), + drivers: makeDrivers, + attributes() { // TODO add variability to these return { @@ -72,5 +75,42 @@ export default Factory.extend({ server.create('client-stats', { id: node.httpAddr, }); + + const events = server.createList('node-event', faker.random.number({ min: 1, max: 10 }), { + nodeId: node.id, + }); + + node.update({ + eventIds: events.mapBy('id'), + }); }, }); + +function makeDrivers() { + const generate = () => { + const detected = Math.random() > 0.3; + const healthy = detected && Math.random() > 0.3; + const attributes = { + 'driver.name.version': '1.0.0', + 'driver.name.status': 'awesome', + 'driver.name.more.details': 'yeah', + 'driver.name.more.again': 'we got that', + }; + return { + Detected: detected, + Healthy: healthy, + HealthDescription: healthy ? 'Driver is healthy' : 'Uh oh', + UpdateTime: faker.date.past(5 / 365, REF_DATE), + Attributes: Math.random() > 0.3 && detected ? attributes : null, + }; + }; + + return { + docker: generate(), + rkt: generate(), + qemu: generate(), + exec: generate(), + raw_exec: generate(), + java: generate(), + }; +} diff --git a/ui/mirage/factories/task-event.js b/ui/mirage/factories/task-event.js index ab161cdb2..72397110c 100644 --- a/ui/mirage/factories/task-event.js +++ b/ui/mirage/factories/task-event.js @@ -1,4 +1,4 @@ -import { Factory, faker, trait } from 'ember-cli-mirage'; +import { Factory, faker } from 'ember-cli-mirage'; import { provide } from '../utils'; const REF_TIME = new Date(); diff --git a/ui/mirage/factories/task.js b/ui/mirage/factories/task.js index 47afe3bbd..49733e938 100644 --- a/ui/mirage/factories/task.js +++ b/ui/mirage/factories/task.js @@ -1,6 +1,8 @@ import { Factory, faker } from 'ember-cli-mirage'; import { generateResources } from '../common'; +const DRIVERS = ['docker', 'java', 'rkt', 'qemu', 'exec', 'raw_exec']; + export default Factory.extend({ // Hidden property used to compute the Summary hash groupNames: [], @@ -8,6 +10,7 @@ export default Factory.extend({ JobID: '', name: id => `task-${faker.hacker.noun()}-${id}`, + driver: faker.list.random(...DRIVERS), Resources: generateResources, }); diff --git a/ui/mirage/models/node.js b/ui/mirage/models/node.js new file mode 100644 index 000000000..e42622483 --- /dev/null +++ b/ui/mirage/models/node.js @@ -0,0 +1,5 @@ +import { Model, hasMany } from 'ember-cli-mirage'; + +export default Model.extend({ + events: hasMany('node-event'), +}); diff --git a/ui/mirage/serializers/node.js b/ui/mirage/serializers/node.js new file mode 100644 index 000000000..506c19038 --- /dev/null +++ b/ui/mirage/serializers/node.js @@ -0,0 +1,6 @@ +import ApplicationSerializer from './application'; + +export default ApplicationSerializer.extend({ + embed: true, + include: ['events'], +}); From 9b45d35e8fa4be3dcbafe06bd6713919df07b89d Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Sat, 12 May 2018 20:00:34 -0700 Subject: [PATCH 18/27] Disable visibility behaviors when testing It results in surprise behaviors. --- ui/app/mixins/with-component-visibility-detection.js | 11 ++++++++--- ui/app/mixins/with-route-visibility-detection.js | 11 ++++++++--- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/ui/app/mixins/with-component-visibility-detection.js b/ui/app/mixins/with-component-visibility-detection.js index 66d1097ee..605937834 100644 --- a/ui/app/mixins/with-component-visibility-detection.js +++ b/ui/app/mixins/with-component-visibility-detection.js @@ -1,3 +1,4 @@ +import Ember from 'ember'; import Mixin from '@ember/object/mixin'; import { assert } from '@ember/debug'; @@ -7,11 +8,15 @@ export default Mixin.create({ }, setupDocumentVisibility: function() { - this.set('_visibilityHandler', this.get('visibilityHandler').bind(this)); - document.addEventListener('visibilitychange', this.get('_visibilityHandler')); + if (!Ember.testing) { + this.set('_visibilityHandler', this.get('visibilityHandler').bind(this)); + document.addEventListener('visibilitychange', this.get('_visibilityHandler')); + } }.on('init'), removeDocumentVisibility: function() { - document.removeEventListener('visibilitychange', this.get('_visibilityHandler')); + if (!Ember.testing) { + document.removeEventListener('visibilitychange', this.get('_visibilityHandler')); + } }.on('willDestroy'), }); diff --git a/ui/app/mixins/with-route-visibility-detection.js b/ui/app/mixins/with-route-visibility-detection.js index 1df6e5f45..57e8300e3 100644 --- a/ui/app/mixins/with-route-visibility-detection.js +++ b/ui/app/mixins/with-route-visibility-detection.js @@ -1,3 +1,4 @@ +import Ember from 'ember'; import Mixin from '@ember/object/mixin'; import { assert } from '@ember/debug'; @@ -7,11 +8,15 @@ export default Mixin.create({ }, setupDocumentVisibility: function() { - this.set('_visibilityHandler', this.get('visibilityHandler').bind(this)); - document.addEventListener('visibilitychange', this.get('_visibilityHandler')); + if (!Ember.testing) { + this.set('_visibilityHandler', this.get('visibilityHandler').bind(this)); + document.addEventListener('visibilitychange', this.get('_visibilityHandler')); + } }.on('activate'), removeDocumentVisibility: function() { - document.removeEventListener('visibilitychange', this.get('_visibilityHandler')); + if (!Ember.testing) { + document.removeEventListener('visibilitychange', this.get('_visibilityHandler')); + } }.on('deactivate'), }); From 37a235c2af3b77b17e3adb594d82f294d1d80e70 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Sat, 12 May 2018 20:01:51 -0700 Subject: [PATCH 19/27] Acceptance tests for node driver and node events --- .../list-accordion/accordion-head.js | 2 + .../allocations/allocation/index.hbs | 2 +- ui/app/templates/clients/client.hbs | 24 +-- .../templates/components/allocation-row.hbs | 4 +- .../list-accordion/accordion-body.hbs | 2 +- .../list-accordion/accordion-head.hbs | 1 + ui/tests/acceptance/allocation-detail-test.js | 22 +- ui/tests/acceptance/client-detail-test.js | 193 ++++++++++++++++-- ui/tests/acceptance/task-group-detail-test.js | 9 +- ui/tests/integration/allocation-row-test.js | 40 ++++ ui/tests/unit/adapters/node-test.js | 4 + ui/tests/unit/serializers/node-test.js | 8 +- 12 files changed, 278 insertions(+), 33 deletions(-) diff --git a/ui/app/components/list-accordion/accordion-head.js b/ui/app/components/list-accordion/accordion-head.js index 4de6d4edc..9cff6e92b 100644 --- a/ui/app/components/list-accordion/accordion-head.js +++ b/ui/app/components/list-accordion/accordion-head.js @@ -4,6 +4,8 @@ export default Component.extend({ classNames: ['accordion-head'], classNameBindings: ['isOpen::is-light', 'isExpandable::is-inactive'], + 'data-test-accordion-head': true, + buttonLabel: 'toggle', isOpen: false, isExpandable: true, diff --git a/ui/app/templates/allocations/allocation/index.hbs b/ui/app/templates/allocations/allocation/index.hbs index b95afd44c..c2e0a60ce 100644 --- a/ui/app/templates/allocations/allocation/index.hbs +++ b/ui/app/templates/allocations/allocation/index.hbs @@ -51,7 +51,7 @@ {{#if (not row.model.driverStatus.healthy)}} - + {{x-icon "warning" class="is-warning"}} {{/if}} diff --git a/ui/app/templates/clients/client.hbs b/ui/app/templates/clients/client.hbs index 79a18e34c..00da3de68 100644 --- a/ui/app/templates/clients/client.hbs +++ b/ui/app/templates/clients/client.hbs @@ -93,7 +93,7 @@
-
+
Client Events
@@ -105,10 +105,10 @@ Message {{/t.head}} {{#t.body as |row|}} - - {{moment-format row.model.time "MM/DD/YY HH:mm:ss"}} - {{row.model.subsystem}} - + + {{moment-format row.model.time "MM/DD/YY HH:mm:ss"}} + {{row.model.subsystem}} + {{#if row.model.message}} {{row.model.message}} {{else}} @@ -121,7 +121,7 @@
-
+
Driver Status
@@ -130,11 +130,11 @@ {{#a.head buttonLabel="details" isExpandable=a.item.detected}}
- {{a.item.name}} + {{a.item.name}}
{{#if a.item.detected}} - + {{if a.item.healthy "Healthy" "Unhealthy"}} @@ -143,20 +143,20 @@
Detected - {{if a.item.detected "Yes" "No"}} + {{if a.item.detected "Yes" "No"}} Last Updated - {{moment-from-now a.item.updateTime interval=1000}} + {{moment-from-now a.item.updateTime interval=1000}}
{{/a.head}} {{#a.body}} -

{{a.item.healthDescription}}

-
+

{{a.item.healthDescription}}

+
{{capitalize a.item.name}} Attributes
diff --git a/ui/app/templates/components/allocation-row.hbs b/ui/app/templates/components/allocation-row.hbs index 8f8590702..1da565ff5 100644 --- a/ui/app/templates/components/allocation-row.hbs +++ b/ui/app/templates/components/allocation-row.hbs @@ -1,11 +1,11 @@ {{#if allocation.unhealthyDrivers.length}} - + {{x-icon "warning" class="is-warning"}} {{/if}} {{#if allocation.nextAllocation}} - + {{x-icon "history" class="is-faded"}} {{/if}} diff --git a/ui/app/templates/components/list-accordion/accordion-body.hbs b/ui/app/templates/components/list-accordion/accordion-body.hbs index 894e66818..35512b2b7 100644 --- a/ui/app/templates/components/list-accordion/accordion-body.hbs +++ b/ui/app/templates/components/list-accordion/accordion-body.hbs @@ -1,5 +1,5 @@ {{#if isOpen}} -
+
{{yield}}
{{/if}} diff --git a/ui/app/templates/components/list-accordion/accordion-head.hbs b/ui/app/templates/components/list-accordion/accordion-head.hbs index a362b1165..5d5cab568 100644 --- a/ui/app/templates/components/list-accordion/accordion-head.hbs +++ b/ui/app/templates/components/list-accordion/accordion-head.hbs @@ -2,6 +2,7 @@ {{yield}}
{{else if isPendingConfirmation}} From 36fc53c057ae5df1d8068e8b6e21f5d9180e20e0 Mon Sep 17 00:00:00 2001 From: Chelsea Holland Komlo Date: Tue, 29 May 2018 21:30:49 -0400 Subject: [PATCH 25/27] fixup! clearify docs and group similar TLS fields --- helper/tlsutil/config.go | 6 +++--- website/source/docs/agent/configuration/tls.html.md | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/helper/tlsutil/config.go b/helper/tlsutil/config.go index dfd1ba6ab..07c1e997b 100644 --- a/helper/tlsutil/config.go +++ b/helper/tlsutil/config.go @@ -105,14 +105,14 @@ type Config struct { // these values for acceptable safe alternatives. CipherSuites []uint16 - // MinVersion contains the minimum SSL/TLS version that is accepted. - MinVersion uint16 - // PreferServerCipherSuites controls whether the server selects the // client's most preferred ciphersuite, or the server's most preferred // ciphersuite. If true then the server's preference, as expressed in // the order of elements in CipherSuites, is used. PreferServerCipherSuites bool + + // MinVersion contains the minimum SSL/TLS version that is accepted. + MinVersion uint16 } func NewTLSConfiguration(newConf *config.TLSConfig, verifyIncoming, verifyOutgoing bool) (*Config, error) { diff --git a/website/source/docs/agent/configuration/tls.html.md b/website/source/docs/agent/configuration/tls.html.md index 8ec43a2ab..07deb14f3 100644 --- a/website/source/docs/agent/configuration/tls.html.md +++ b/website/source/docs/agent/configuration/tls.html.md @@ -67,8 +67,8 @@ the [Agent's Gossip and RPC Encryption](/docs/agent/encryption.html). - `tls_min_version` - Specifies the minimum supported version of TLS. Accepted values are "tls10", "tls11", "tls12". Defaults to TLS 1.2. -- tls_prefer_server_cipher_suites - This option will cause Nomad to prefer the - server's ciphersuite over the client ciphersuites. +- `tls_prefer_server_cipher_suites` - Specifies whether TLS connections should + prefer the server's ciphersuites over the client's. Defaults to false. - `verify_https_client` `(bool: false)` - Specifies agents should require client certificates for all incoming HTTPS requests. The client certificates From ef6f6cfedbfe0d9209d723a535d7f0b34f6adbb6 Mon Sep 17 00:00:00 2001 From: Chelsea Holland Komlo Date: Tue, 29 May 2018 21:35:42 -0400 Subject: [PATCH 26/27] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 812e2bee3..8a06b68c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ IMPROVEMENTS: * core: Updated serf library to improve how leave intents are handled [[GH-4278](https://github.com/hashicorp/nomad/issues/4278)] + * core: Added TLS configuration option to prefer server's ciphersuites over clients[[GH-4338](https://github.com/hashicorp/nomad/issues/4338)] * core: Add the option for operators to configure TLS versions and allowed cipher suites. Default is a subset of safe ciphers and TLS 1.2 [[GH-4269](https://github.com/hashicorp/nomad/pull/4269)] * core: Add a new [progress_deadline](https://www.nomadproject.io/docs/job-specification/update.html#progress_deadline) parameter to From e056eec67d87a1f13e74b44fe74dedc4411f8a55 Mon Sep 17 00:00:00 2001 From: Alex Dadgar Date: Wed, 30 May 2018 10:23:41 -0700 Subject: [PATCH 27/27] Fix docs for defaults --- .../source/docs/agent/configuration/tls.html.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/website/source/docs/agent/configuration/tls.html.md b/website/source/docs/agent/configuration/tls.html.md index 07deb14f3..a5f049bea 100644 --- a/website/source/docs/agent/configuration/tls.html.md +++ b/website/source/docs/agent/configuration/tls.html.md @@ -58,17 +58,18 @@ the [Agent's Gossip and RPC Encryption](/docs/agent/encryption.html). cluster is being upgraded to TLS, and removed after the migration is complete. This allows the agent to accept both TLS and plaintext traffic. -- `tls_cipher_suites` - Specifies the TLS cipher suites that will be used by - the agent. Known insecure ciphers are disabled (3DES and RC4). By default, - an agent is configured to use TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, +- `tls_cipher_suites` `(array: [])` - Specifies the TLS cipher suites + that will be used by the agent. Known insecure ciphers are disabled (3DES and + RC4). By default, an agent is configured to use + TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, and TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384. -- `tls_min_version` - Specifies the minimum supported version of TLS. Accepted - values are "tls10", "tls11", "tls12". Defaults to TLS 1.2. +- `tls_min_version` `(string: "tls12")`- Specifies the minimum supported version + of TLS. Accepted values are "tls10", "tls11", "tls12". -- `tls_prefer_server_cipher_suites` - Specifies whether TLS connections should - prefer the server's ciphersuites over the client's. Defaults to false. +- `tls_prefer_server_cipher_suites` `(bool: false)` - Specifies whether + TLS connections should prefer the server's ciphersuites over the client's. - `verify_https_client` `(bool: false)` - Specifies agents should require client certificates for all incoming HTTPS requests. The client certificates