Skip to content

Commit 06baadd

Browse files
authored
UI: add filesystem browsing for allocations (#7951)
This partially addresses #7799. Task state filesystems are contained within a subdirectory of their parent allocation, so almost everything that existed for browsing task state filesystems was applicable to browsing allocations, just without the task name prepended to the path. I aimed to push this differential handling into as few contained places as possible. The tests also have significant overlap, so this includes an extracted behavior to run the same tests for allocations and task states.
1 parent 3b04afe commit 06baadd

File tree

34 files changed

+786
-491
lines changed

34 files changed

+786
-491
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ IMPROVEMENTS:
1212
* csi: Move volume claim releases out of evaluation workers [[GH-8021](https://github.com/hashicorp/nomad/issues/8021)]
1313
* csi: Added support for `VolumeContext` and `VolumeParameters` [[GH-7957](https://github.com/hashicorp/nomad/issues/7957)]
1414
* logging: Remove spurious error log on task shutdown [[GH-8028](https://github.com/hashicorp/nomad/issues/8028)]
15+
* ui: Added filesystem browsing for allocations [[GH-5871](https://github.com/hashicorp/nomad/pull/7951)]
1516

1617
BUG FIXES:
1718

ui/app/adapters/allocation.js

+33
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,41 @@ export default Watchable.extend({
1111
data: taskName && { TaskName: taskName },
1212
});
1313
},
14+
15+
ls(model, path) {
16+
return this.token
17+
.authorizedRequest(`/v1/client/fs/ls/${model.id}?path=${encodeURIComponent(path)}`)
18+
.then(handleFSResponse);
19+
},
20+
21+
stat(model, path) {
22+
return this.token
23+
.authorizedRequest(
24+
`/v1/client/fs/stat/${model.id}?path=${encodeURIComponent(path)}`
25+
)
26+
.then(handleFSResponse);
27+
},
1428
});
1529

30+
async function handleFSResponse(response) {
31+
if (response.ok) {
32+
return response.json();
33+
} else {
34+
const body = await response.text();
35+
36+
// TODO update this if/when endpoint returns 404 as expected
37+
const statusIs500 = response.status === 500;
38+
const bodyIncludes404Text = body.includes('no such file or directory');
39+
40+
const translatedCode = statusIs500 && bodyIncludes404Text ? 404 : response.status;
41+
42+
throw {
43+
code: translatedCode,
44+
toString: () => body,
45+
};
46+
}
47+
}
48+
1649
function adapterAction(path, verb = 'POST') {
1750
return function(allocation) {
1851
const url = addToPath(this.urlForFindRecord(allocation.id, 'allocation'), path);

ui/app/adapters/task-state.js

-39
This file was deleted.
+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import Component from '@ember/component';
2+
import { inject as service } from '@ember/service';
3+
import { equal, or } from '@ember/object/computed';
4+
5+
export default Component.extend({
6+
router: service(),
7+
8+
tagName: '',
9+
10+
fsIsActive: equal('router.currentRouteName', 'allocations.allocation.fs'),
11+
fsRootIsActive: equal('router.currentRouteName', 'allocations.allocation.fs-root'),
12+
13+
filesLinkActive: or('fsIsActive', 'fsRootIsActive'),
14+
});

ui/app/components/fs-breadcrumbs.js ui/app/components/fs/breadcrumbs.js

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export default Component.extend({
88

99
'data-test-fs-breadcrumbs': true,
1010

11+
allocation: null,
1112
task: null,
1213
path: null,
1314

ui/app/components/fs/browser.js

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import Component from '@ember/component';
2+
import { computed } from '@ember/object';
3+
import { filterBy } from '@ember/object/computed';
4+
5+
export default Component.extend({
6+
tagName: '',
7+
8+
model: null,
9+
10+
allocation: computed('model', function() {
11+
if (this.model.allocation) {
12+
return this.model.allocation;
13+
} else {
14+
return this.model;
15+
}
16+
}),
17+
18+
task: computed('model', function() {
19+
if (this.model.allocation) {
20+
return this.model;
21+
}
22+
}),
23+
24+
type: computed('task', function() {
25+
if (this.task) {
26+
return 'task';
27+
} else {
28+
return 'allocation';
29+
}
30+
}),
31+
32+
directories: filterBy('directoryEntries', 'IsDir'),
33+
files: filterBy('directoryEntries', 'IsDir', false),
34+
35+
sortedDirectoryEntries: computed(
36+
'directoryEntries.[]',
37+
'sortProperty',
38+
'sortDescending',
39+
function() {
40+
const sortProperty = this.sortProperty;
41+
42+
const directorySortProperty = sortProperty === 'Size' ? 'Name' : sortProperty;
43+
44+
const sortedDirectories = this.directories.sortBy(directorySortProperty);
45+
const sortedFiles = this.files.sortBy(sortProperty);
46+
47+
const sortedDirectoryEntries = sortedDirectories.concat(sortedFiles);
48+
49+
if (this.sortDescending) {
50+
return sortedDirectoryEntries.reverse();
51+
} else {
52+
return sortedDirectoryEntries;
53+
}
54+
}
55+
),
56+
57+
});

ui/app/components/fs-directory-entry.js ui/app/components/fs/directory-entry.js

+3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import { isEmpty } from '@ember/utils';
55
export default Component.extend({
66
tagName: '',
77

8+
allocation: null,
9+
task: null,
10+
811
pathToEntry: computed('path', 'entry.Name', function() {
912
const pathWithNoLeadingSlash = this.get('path').replace(/^\//, '');
1013
const name = encodeURIComponent(this.get('entry.Name'));

ui/app/components/task-file.js ui/app/components/fs/file.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ export default Component.extend({
4949
isStreaming: false,
5050

5151
catUrl: computed('allocation.id', 'task.name', 'file', function() {
52-
const encodedPath = encodeURIComponent(`${this.task.name}/${this.file}`);
52+
const taskUrlPrefix = this.task ? `${this.task.name}/` : '';
53+
const encodedPath = encodeURIComponent(`${taskUrlPrefix}${this.file}`);
5354
return `/v1/client/fs/cat/${this.allocation.id}?path=${encodedPath}`;
5455
}),
5556

@@ -79,7 +80,8 @@ export default Component.extend({
7980

8081
fileParams: computed('task.name', 'file', 'mode', function() {
8182
// The Log class handles encoding query params
82-
const path = `${this.task.name}/${this.file}`;
83+
const taskUrlPrefix = this.task ? `${this.task.name}/` : '';
84+
const path = `${taskUrlPrefix}${this.file}`;
8385

8486
switch (this.mode) {
8587
case 'head':

ui/app/components/fs/link.js

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import Component from '@ember/component';
2+
3+
export default Component.extend({
4+
tagName: '',
5+
6+
allocation: null,
7+
task: null,
8+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import FSController from './fs';
2+
3+
export default FSController.extend();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import Controller from '@ember/controller';
2+
import { computed } from '@ember/object';
3+
4+
export default Controller.extend({
5+
queryParams: {
6+
sortProperty: 'sort',
7+
sortDescending: 'desc',
8+
},
9+
10+
sortProperty: 'Name',
11+
sortDescending: false,
12+
13+
path: null,
14+
allocation: null,
15+
directoryEntries: null,
16+
isFile: null,
17+
stat: null,
18+
19+
pathWithLeadingSlash: computed('path', function() {
20+
const path = this.path;
21+
22+
if (path.startsWith('/')) {
23+
return path;
24+
} else {
25+
return `/${path}`;
26+
}
27+
}),
28+
});
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import Controller from '@ember/controller';
22
import { computed } from '@ember/object';
3-
import { filterBy } from '@ember/object/computed';
43

54
export default Controller.extend({
65
queryParams: {
@@ -17,9 +16,6 @@ export default Controller.extend({
1716
isFile: null,
1817
stat: null,
1918

20-
directories: filterBy('directoryEntries', 'IsDir'),
21-
files: filterBy('directoryEntries', 'IsDir', false),
22-
2319
pathWithLeadingSlash: computed('path', function() {
2420
const path = this.path;
2521

@@ -29,26 +25,4 @@ export default Controller.extend({
2925
return `/${path}`;
3026
}
3127
}),
32-
33-
sortedDirectoryEntries: computed(
34-
'directoryEntries.[]',
35-
'sortProperty',
36-
'sortDescending',
37-
function() {
38-
const sortProperty = this.sortProperty;
39-
40-
const directorySortProperty = sortProperty === 'Size' ? 'Name' : sortProperty;
41-
42-
const sortedDirectories = this.directories.sortBy(directorySortProperty);
43-
const sortedFiles = this.files.sortBy(sortProperty);
44-
45-
const sortedDirectoryEntries = sortedDirectories.concat(sortedFiles);
46-
47-
if (this.sortDescending) {
48-
return sortedDirectoryEntries.reverse();
49-
} else {
50-
return sortedDirectoryEntries;
51-
}
52-
}
53-
),
5428
});

ui/app/models/allocation.js

+8
Original file line numberDiff line numberDiff line change
@@ -125,4 +125,12 @@ export default Model.extend({
125125
restart(taskName) {
126126
return this.store.adapterFor('allocation').restart(this, taskName);
127127
},
128+
129+
ls(path) {
130+
return this.store.adapterFor('allocation').ls(this, path);
131+
},
132+
133+
stat(path) {
134+
return this.store.adapterFor('allocation').stat(this, path);
135+
},
128136
});

ui/app/models/task-state.js

-8
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,4 @@ export default Fragment.extend({
5151
restart() {
5252
return this.allocation.restart(this.name);
5353
},
54-
55-
ls(path) {
56-
return this.store.adapterFor('task-state').ls(this, path);
57-
},
58-
59-
stat(path) {
60-
return this.store.adapterFor('task-state').stat(this, path);
61-
},
6254
});

ui/app/router.js

+3
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ Router.map(function() {
4747

4848
this.route('allocations', function() {
4949
this.route('allocation', { path: '/:allocation_id' }, function() {
50+
this.route('fs-root', { path: '/fs' });
51+
this.route('fs', { path: '/fs/*path' });
52+
5053
this.route('task', { path: '/:name' }, function() {
5154
this.route('logs');
5255
this.route('fs-root', { path: '/fs' });
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import FSRoute from './fs';
2+
3+
export default FSRoute.extend({
4+
templateName: 'allocations/allocation/fs',
5+
});
+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import Route from '@ember/routing/route';
2+
import RSVP from 'rsvp';
3+
import notifyError from 'nomad-ui/utils/notify-error';
4+
5+
export default Route.extend({
6+
model({ path = '/' }) {
7+
const decodedPath = decodeURIComponent(path);
8+
const allocation = this.modelFor('allocations.allocation');
9+
10+
if (!allocation.isRunning) {
11+
return {
12+
path: decodedPath,
13+
allocation,
14+
};
15+
}
16+
17+
return RSVP.all([allocation.stat(decodedPath), allocation.get('node')])
18+
.then(([statJson]) => {
19+
if (statJson.IsDir) {
20+
return RSVP.hash({
21+
path: decodedPath,
22+
allocation,
23+
directoryEntries: allocation.ls(decodedPath).catch(notifyError(this)),
24+
isFile: false,
25+
});
26+
} else {
27+
return {
28+
path: decodedPath,
29+
allocation,
30+
isFile: true,
31+
stat: statJson,
32+
};
33+
}
34+
})
35+
.catch(notifyError(this));
36+
},
37+
38+
setupController(controller, { path, allocation, directoryEntries, isFile, stat } = {}) {
39+
this._super(...arguments);
40+
controller.setProperties({ path, allocation, directoryEntries, isFile, stat });
41+
},
42+
});

ui/app/routes/allocations/allocation/task/fs.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export default Route.extend({
66
model({ path = '/' }) {
77
const decodedPath = decodeURIComponent(path);
88
const task = this.modelFor('allocations.allocation.task');
9+
const allocation = task.allocation;
910

1011
const pathWithTaskName = `${task.name}${decodedPath.startsWith('/') ? '' : '/'}${decodedPath}`;
1112

@@ -16,13 +17,13 @@ export default Route.extend({
1617
};
1718
}
1819

19-
return RSVP.all([task.stat(pathWithTaskName), task.get('allocation.node')])
20+
return RSVP.all([allocation.stat(pathWithTaskName), task.get('allocation.node')])
2021
.then(([statJson]) => {
2122
if (statJson.IsDir) {
2223
return RSVP.hash({
2324
path: decodedPath,
2425
task,
25-
directoryEntries: task.ls(pathWithTaskName).catch(notifyError(this)),
26+
directoryEntries: allocation.ls(pathWithTaskName).catch(notifyError(this)),
2627
isFile: false,
2728
});
2829
} else {

0 commit comments

Comments
 (0)