-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Changes from all commits
1c07959
67a6216
25977e4
51acf0a
50deac2
6b170a0
fd6f001
9a375c2
b73a4d1
0ffc56a
77b132c
de8557e
95805d2
0bb3284
0eaad67
b5f7029
7f7dcb7
be8012c
41fdcfa
9f6ab40
c0a8b4a
2c3b4e2
a62399e
0eaf5a2
a2a48df
b2937f5
12e6f57
86cda1c
dd190a9
3826bf3
4f706c2
b0ad848
44840a1
665941a
8a05161
7fa42ce
b6e2187
e9c1ed8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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'; | ||
}, | ||
}); |
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 } }); | ||
}, | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import Component from '@ember/component'; | ||
import filesize from 'filesize'; | ||
|
||
/** | ||
* @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); | ||
}, | ||
}, | ||
}); |
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'); | ||
}, | ||
}, | ||
}); |
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.'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
if this message merely sets a flash message, where does the download actually happen? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
}, | ||
}, | ||
}); |
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(); | ||
}, | ||
}, | ||
}); |
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', | ||
]); | ||
}), | ||
}); |
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'), | ||
}); |
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(); | ||
}, | ||
}, | ||
}); |
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); | ||
}, | ||
}); |
There was a problem hiding this comment.
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?There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 !