Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI - raft config and snapshotting #7410

Merged
merged 38 commits into from
Oct 14, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
1c07959
add storage route
meirish Aug 22, 2019
67a6216
template out the routes and new raft storage overview
meirish Aug 27, 2019
25977e4
fetch raft config and add new server model
meirish Aug 27, 2019
51acf0a
pngcrush the favicon
meirish Aug 27, 2019
50deac2
add view components and binary-file component
meirish Aug 27, 2019
6b170a0
add form-save-buttons component
meirish Aug 29, 2019
fd6f001
adjust rawRequest so that it can send a request body and returns the …
meirish Aug 29, 2019
9a375c2
hook up restore
meirish Aug 29, 2019
b73a4d1
rename binary-file to file-to-array-buffer
meirish Aug 29, 2019
0ffc56a
add ember-service-worker
meirish Aug 30, 2019
77b132c
use forked version of ember-service-worker for now
meirish Aug 31, 2019
de8557e
scope the service worker to a single endpoint
meirish Aug 31, 2019
95805d2
show both download buttons for now
meirish Aug 31, 2019
0bb3284
add service worker download with a fallback to JS in-mem download
meirish Sep 3, 2019
0eaad67
add remove peer functionality
meirish Sep 3, 2019
b5f7029
lint go file
meirish Sep 10, 2019
7f7dcb7
add storage-type to the cluster and node models
meirish Sep 13, 2019
be8012c
update edit for to take a cancel action
meirish Sep 13, 2019
41fdcfa
separate out a css table styles to be used by http-requests-table and…
meirish Sep 16, 2019
9f6ab40
add raft-join adapter, model, component and use on the init page
meirish Sep 16, 2019
c0a8b4a
fix styling and gate the menu item on the cluster using raft storage
meirish Sep 17, 2019
2c3b4e2
style tweaks to the raft-join component
meirish Sep 17, 2019
a62399e
fix linting
meirish Sep 19, 2019
0eaf5a2
add form-save-buttons component to storybook
meirish Sep 27, 2019
a2a48df
add cancel functionality for backup uploads, and add a success messag…
meirish Sep 27, 2019
b2937f5
add component tests
meirish Sep 27, 2019
12e6f57
add filesize.js
meirish Sep 27, 2019
86cda1c
add filesize and modified date to file-to-array-buffer
meirish Sep 27, 2019
dd190a9
fix linting
meirish Sep 30, 2019
3826bf3
fix server section showing in the cluster nav
meirish Sep 30, 2019
4f706c2
don't use babel transforms in service worker lib because we don't wan…
meirish Sep 30, 2019
b0ad848
add file-to-array-buffer to storybook
meirish Oct 10, 2019
44840a1
add comments and use removeObjectURL to raft-storage-overview
meirish Oct 10, 2019
665941a
update alert-banner markdown
meirish Oct 10, 2019
8a05161
messaging change for upload alert banner
meirish Oct 10, 2019
7fa42ce
Update ui/app/templates/components/raft-storage-restore.hbs
meirish Oct 10, 2019
b6e2187
more comments
meirish Oct 10, 2019
e9c1ed8
actually render the label if passed and update stories with knobs
meirish Oct 11, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion ui/app/adapters/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,13 @@ export default DS.RESTAdapter.extend(AdapterFetch, {
return fetch(url, {
method: type || 'GET',
headers: opts.headers || {},
body: opts.body,
signal: opts.signal,
}).then(response => {
if (response.status >= 200 && response.status < 300) {
return RSVP.resolve(response);
} else {
return RSVP.reject();
return RSVP.reject(response);
}
});
},
Expand Down
7 changes: 7 additions & 0 deletions ui/app/adapters/raft-join.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import ApplicationAdapter from './application';

export default ApplicationAdapter.extend({
urlForCreateRecord() {
return '/v1/sys/storage/raft/join';
},
});
15 changes: 15 additions & 0 deletions ui/app/adapters/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import ApplicationAdapter from './application';

export default ApplicationAdapter.extend({
urlForFindAll() {
return '/v1/sys/storage/raft/configuration';
},
urlForDeleteRecord() {
return '/v1/sys/storage/raft/remove-peer';
},
deleteRecord(store, type, snapshot) {
let server_id = snapshot.attr('nodeId');
let url = '/v1/sys/storage/raft/remove-peer';
return this.ajax(url, 'POST', { data: { server_id } });
},
});
59 changes: 59 additions & 0 deletions ui/app/components/file-to-array-buffer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import Component from '@ember/component';
import filesize from 'filesize';

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i saw that you added the FormSaveButtons -- should this component be added to Storybook too? even if you decide not to add it, could you add JSDoc comments here so we know what the component is for / what the properties are, etc?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah there are JS doc comments (the old style), but I should convert this and add it to Storybook.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated and added to storybook in 66123c3 !

/**
* @module FileToArrayBuffer
* `FileToArrayBuffer` is a component that will allow you to pick a file from the local file system. Once
* loaded, this file will be emitted as a JS ArrayBuffer to the passed `onChange` callback.
*
* @example
* ```js
* <FileToArrayBuffer @onChange={{action (mut file)}} />
* ```
* @param onChange=null {Function} - The function to call when the file read is complete. This function
* recieves the file as a JS ArrayBuffer
* @param [label=null {String}] - Text to use as the label for the file input
* @param [fileHelpText=null {String} - Text to use as help under the file input
*
*/
export default Component.extend({
classNames: ['box', 'is-fullwidth', 'is-marginless', 'is-shadowless'],
onChange: () => {},
label: null,
fileHelpText: null,

file: null,
fileName: null,
fileSize: null,
fileLastModified: null,

readFile(file) {
const reader = new FileReader();
reader.onload = () => this.send('onChange', reader.result, file);
reader.readAsArrayBuffer(file);
meirish marked this conversation as resolved.
Show resolved Hide resolved
},

actions: {
pickedFile(e) {
let { files } = e.target;
if (!files.length) {
return;
}
for (let i = 0, len = files.length; i < len; i++) {
this.readFile(files[i]);
}
},
clearFile() {
this.send('onChange');
},
onChange(fileAsBytes, fileMeta) {
let { name, size, lastModifiedDate } = fileMeta || {};
let fileSize = size ? filesize(size) : null;
this.set('file', fileAsBytes);
this.set('fileName', name);
this.set('fileSize', fileSize);
this.set('fileLastModified', lastModifiedDate);
this.onChange(fileAsBytes, name);
},
},
});
38 changes: 38 additions & 0 deletions ui/app/components/raft-join.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { inject as service } from '@ember/service';

/**
* @module RaftJoin
* RaftJoin component presents the user with a choice to join an existing raft cluster when a new Vault
* server is brought up
*
*
* @example
* ```js
* <RaftJoin @onDismiss={{action (mut attr)}} />
* ```
* @param {function} onDismiss - This function will be called if the user decides not to join an existing
* raft cluster
*
*/

import Component from '@ember/component';

export default Component.extend({
classNames: 'raft-join',
store: service(),
onDismiss() {},
preference: 'join',
showJoinForm: false,
actions: {
advanceFirstScreen() {
if (this.preference !== 'join') {
this.onDismiss();
return;
}
this.set('showJoinForm', true);
},
newModel() {
return this.store.createRecord('raft-join');
},
},
});
81 changes: 81 additions & 0 deletions ui/app/components/raft-storage-overview.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import Component from '@ember/component';
import { getOwner } from '@ember/application';
import config from '../config/environment';
import { inject as service } from '@ember/service';

export default Component.extend({
flashMessages: service(),
useServiceWorker: null,

async init() {
this._super(...arguments);
if (this.useServiceWorker === false) {
return;
}
// check to see if we support ServiceWorker
if ('serviceWorker' in navigator) {
meirish marked this conversation as resolved.
Show resolved Hide resolved
// this checks to see if there's an active service worker - if it failed to register
// for any reason, then this would be null
let worker = await navigator.serviceWorker.getRegistration(config.serviceWorkerScope);
if (worker) {
this.set('useServiceWorker', true);
}
}
},

actions: {
async removePeer(model) {
let { nodeId } = model;
try {
await model.destroyRecord();
} catch (e) {
let errString = e.errors ? e.errors.join(' ') : e.message || e;
this.flashMessages.danger(`There was an issue removing the peer ${nodeId}: ${errString}`);
return;
}
this.flashMessages.success(`Successfully removed the peer: ${nodeId}.`);
},

downloadViaServiceWorker() {
// the actual download happens when the user clicks the anchor link, and then the ServiceWorker
// intercepts the request and adds auth headers.
// Here we just want to notify users that something is happening before the browser starts the download
this.flashMessages.success('The snapshot download will begin shortly.');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when i saw the name of this method i expected to see something like:

downloadViaServiceWorker() {
      this.flashMessages.success('The snapshot download will begin shortly.');
      this.initializeDownload() // something that actually kicks off the download
}

if this message merely sets a flash message, where does the download actually happen?
is it happening under the hood because the a element that queues this action has /v1/sys/storage/raft/snapshot as it's href?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, clicking the link links directly to the endpoint and its response has a Content-Disposition header to start a download (I linked the PRs that added that in the description). We can't just link to it though because the endpoint needs a Vault token to be authenticated and there's no native way to do this with an anchor link. The Service Worker we added intercepts this request in a new thread, and communicates to our application thread in order to get the current Vault token, and then adds this the necessary header to the request, allowing the download to work with just the anchor tag.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh nice! thanks for the explanation. in that case could you drop a short comment here explaining that the service worker intercepts the request so we know where to look if we need to debug it later?

},

async downloadSnapshot() {
// this entire method is the fallback behavior in case the browser either doesn't support ServiceWorker
// or the UI is not being run on https.
// here we're downloading the entire snapshot in memory, creating a dataurl with createObjectURL, and
// then forcing a download by clicking a link that has a download attribute
//
// this is not the default because
let adapter = getOwner(this).lookup('adapter:application');

this.flashMessages.success('The snapshot download has begun.');
let resp, blob;
try {
resp = await adapter.rawRequest('/v1/sys/storage/raft/snapshot', 'GET');
blob = await resp.blob();
} catch (e) {
let errString = e.errors ? e.errors.join(' ') : e.message || e;
this.flashMessages.danger(`There was an error trying to download the snapshot: ${errString}`);
}
let filename = 'snapshot.gz';
let file = new Blob([blob], { type: 'application/x-gzip' });
file.name = filename;
if ('msSaveOrOpenBlob' in navigator) {
navigator.msSaveOrOpenBlob(file, filename);
meirish marked this conversation as resolved.
Show resolved Hide resolved
return;
}
let a = document.createElement('a');
let objectURL = window.URL.createObjectURL(file);
a.href = objectURL;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(objectURL);
},
},
});
45 changes: 45 additions & 0 deletions ui/app/components/raft-storage-restore.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import Component from '@ember/component';
import { task } from 'ember-concurrency';
import { getOwner } from '@ember/application';
import { inject as service } from '@ember/service';
import { alias } from '@ember/object/computed';
import { AbortController } from 'fetch';

export default Component.extend({
file: null,
errors: null,
forceRestore: false,
flashMessages: service(),
isUploading: alias('restore.isRunning'),
abortController: null,
restore: task(function*() {
this.set('errors', null);
let adapter = getOwner(this).lookup('adapter:application');
try {
let url = '/v1/sys/storage/raft/snapshot';
if (this.forceRestore) {
url = `${url}-force`;
}
let file = new Blob([this.file], { type: 'application/gzip' });
let controller = new AbortController();
this.set('abortController', controller);
yield adapter.rawRequest(url, 'POST', { body: file, signal: controller.signal });
this.flashMessages.success('The snapshot was successfully uploaded!');
} catch (e) {
if (e.name === 'AbortError') {
return;
}
let resp;
if (e.json) {
resp = yield e.json();
}
let err = resp ? resp.errors : [e];
this.set('errors', err);
}
}),
actions: {
cancelUpload() {
this.abortController.abort();
},
},
});
4 changes: 3 additions & 1 deletion ui/app/models/cluster.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { inject as service } from '@ember/service';
import { not, gte, alias, and, or } from '@ember/object/computed';
import { alias, and, equal, gte, not, or } from '@ember/object/computed';
import { get, computed } from '@ember/object';
import DS from 'ember-data';
import { fragment } from 'ember-data-model-fragments/attributes';
Expand Down Expand Up @@ -38,7 +38,9 @@ export default DS.Model.extend({
sealThreshold: alias('leaderNode.sealThreshold'),
sealProgress: alias('leaderNode.progress'),
sealType: alias('leaderNode.type'),
storageType: alias('leaderNode.storageType'),
hasProgress: gte('sealProgress', 1),
usingRaft: equal('storageType', 'raft'),

//replication mode - will only ever be 'unsupported'
//otherwise the particular mode will have the relevant mode attr through replication-attributes
Expand Down
1 change: 1 addition & 0 deletions ui/app/models/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export default DS.Model.extend({
sealNumShares: alias('n'),
version: attr('string'),
type: attr('string'),
storageType: attr('string'),

//https://www.vaultproject.io/docs/http/sys-leader.html
haEnabled: attr('boolean'),
Expand Down
44 changes: 44 additions & 0 deletions ui/app/models/raft-join.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import DS from 'ember-data';
import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
import { computed } from '@ember/object';
const { attr } = DS;

//leader_api_addr (string: <required>) – Address of the leader node in the Raft cluster to which this node is trying to join.

//retry (bool: false) - Retry joining the Raft cluster in case of failures.

//leader_ca_cert (string: "") - CA certificate used to communicate with Raft's leader node.

//leader_client_cert (string: "") - Client certificate used to communicate with Raft's leader node.

//leader_client_key (string: "") - Client key used to communicate with Raft's leader node.

export default DS.Model.extend({
leaderApiAddr: attr('string', {
label: 'Leader API Address',
}),
retry: attr('boolean', {
label: 'Keep retrying to join in case of failures',
}),
leaderCaCert: attr('string', {
label: 'Leader CA Certificate',
editType: 'file',
}),
leaderClientCert: attr('string', {
label: 'Leader Client Certificate',
editType: 'file',
}),
leaderClientKey: attr('string', {
label: 'Leader Client Key',
editType: 'file',
}),
fields: computed(function() {
return expandAttributeMeta(this, [
'leaderApiAddr',
'leaderCaCert',
'leaderClientCert',
'leaderClientKey',
'retry',
]);
}),
});
11 changes: 11 additions & 0 deletions ui/app/models/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import DS from 'ember-data';
const { attr } = DS;

//{"node_id":"1249bfbc-b234-96f3-0c66-07078ac3e16e","address":"127.0.0.1:8201","leader":true,"protocol_version":"3","voter":true}
export default DS.Model.extend({
address: attr('string'),
nodeId: attr('string'),
protocolVersion: attr('string'),
voter: attr('boolean'),
leader: attr('boolean'),
});
2 changes: 2 additions & 0 deletions ui/app/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ Router.map(function() {
this.mount('open-api-explorer', { path: '/api-explorer' });
this.route('license');
this.route('requests', { path: '/metrics/requests' });
this.route('storage', { path: '/storage/raft' });
this.route('storage-restore', { path: '/storage/raft/restore' });
this.route('settings', function() {
this.route('index', { path: '/' });
this.route('seal');
Expand Down
14 changes: 14 additions & 0 deletions ui/app/routes/vault/cluster/storage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Route from '@ember/routing/route';
import ClusterRoute from 'vault/mixins/cluster-route';

export default Route.extend(ClusterRoute, {
model() {
return this.store.findAll('server');
},

actions: {
doRefresh() {
this.refresh();
},
},
});
13 changes: 13 additions & 0 deletions ui/app/serializers/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import ApplicationSerializer from './application';

export default ApplicationSerializer.extend({
primaryKey: 'node_id',
normalizeItems(payload) {
if (payload.data && payload.data.config) {
// rewrite the payload from data.config.servers to data.keys so we can use the application serializer
// on it
return payload.data.config.servers.slice(0);
}
return this._super(payload);
},
});
1 change: 1 addition & 0 deletions ui/app/services/permissions.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const API_PATHS = {
replication: 'sys/replication',
license: 'sys/license',
seal: 'sys/seal',
raft: 'sys/storage/raft/configuration',
},
metrics: {
requests: 'sys/internal/counters/requests',
Expand Down
Loading