@cloud-cli/vm 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +6 -0
- package/dist/index.js +49 -22
- package/package.json +1 -1
- package/src/index.spec.ts +64 -3
- package/src/index.ts +59 -22
package/dist/index.d.ts
CHANGED
|
@@ -7,6 +7,9 @@ interface NameAndPathOption {
|
|
|
7
7
|
}
|
|
8
8
|
declare function list(): Promise<string[]>;
|
|
9
9
|
declare function ls(options: NameAndPathOption): Promise<string[]>;
|
|
10
|
+
declare function fixPermissions(options: NameOption): Promise<boolean>;
|
|
11
|
+
declare function rm(options: NameAndPathOption): Promise<boolean>;
|
|
12
|
+
declare function cat(options: NameAndPathOption): Promise<string>;
|
|
10
13
|
declare function show(options: NameOption): Promise<{
|
|
11
14
|
name: any;
|
|
12
15
|
createdAt: any;
|
|
@@ -22,5 +25,8 @@ declare const _default: {
|
|
|
22
25
|
show: typeof show;
|
|
23
26
|
prune: typeof prune;
|
|
24
27
|
ls: typeof ls;
|
|
28
|
+
rm: typeof rm;
|
|
29
|
+
cat: typeof cat;
|
|
30
|
+
fixPermissions: typeof fixPermissions;
|
|
25
31
|
};
|
|
26
32
|
export default _default;
|
package/dist/index.js
CHANGED
|
@@ -1,34 +1,61 @@
|
|
|
1
|
-
import { join } from
|
|
2
|
-
import {
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { exec } from '@cloud-cli/exec';
|
|
3
4
|
const nameRe = /^[a-z][a-z0-9-]+$/;
|
|
4
5
|
const isValidName = (name) => nameRe.test(name);
|
|
5
|
-
const invalidNameError = new Error(
|
|
6
|
+
const invalidNameError = new Error('Invalid name');
|
|
6
7
|
async function list() {
|
|
7
|
-
const output = await exec(
|
|
8
|
+
const output = await exec('docker', ['volume', 'ls', '--format={{.Name}}']);
|
|
8
9
|
if (!output.ok) {
|
|
9
|
-
throw new Error(
|
|
10
|
+
throw new Error('Unable to list volumes');
|
|
10
11
|
}
|
|
11
|
-
return output.stdout.trim().split(
|
|
12
|
+
return output.stdout.trim().split('\n');
|
|
12
13
|
}
|
|
13
|
-
async function
|
|
14
|
-
const
|
|
15
|
-
const output = await exec("docker", ["volume", "inspect", name]);
|
|
14
|
+
async function getVolumeMountpoint(name) {
|
|
15
|
+
const output = await exec('docker', ['volume', 'inspect', name]);
|
|
16
16
|
const volumes = JSON.parse(output.stdout);
|
|
17
17
|
if (!volumes.length) {
|
|
18
|
-
throw new Error(
|
|
18
|
+
throw new Error('Volume not found');
|
|
19
|
+
}
|
|
20
|
+
return volumes[0].Mountpoint;
|
|
21
|
+
}
|
|
22
|
+
async function ls(options) {
|
|
23
|
+
const { name, path = '' } = options;
|
|
24
|
+
const root = await getVolumeMountpoint(name);
|
|
25
|
+
const files = await exec('ls', ['-1', join(root, path)]);
|
|
26
|
+
return files.stdout.trim().split('\n');
|
|
27
|
+
}
|
|
28
|
+
async function fixPermissions(options) {
|
|
29
|
+
const { name } = options;
|
|
30
|
+
const root = await getVolumeMountpoint(name);
|
|
31
|
+
const result = await exec('chmod', ['-R', 'a+w', root]);
|
|
32
|
+
return result.ok;
|
|
33
|
+
}
|
|
34
|
+
async function rm(options) {
|
|
35
|
+
const { name, path } = options;
|
|
36
|
+
if (!path) {
|
|
37
|
+
throw new Error('Path not specified');
|
|
38
|
+
}
|
|
39
|
+
const root = await getVolumeMountpoint(name);
|
|
40
|
+
const result = await exec('rm', ['-r', join(root, path)]);
|
|
41
|
+
return result.ok;
|
|
42
|
+
}
|
|
43
|
+
async function cat(options) {
|
|
44
|
+
const { name, path } = options;
|
|
45
|
+
if (!path) {
|
|
46
|
+
throw new Error('Path not specified');
|
|
19
47
|
}
|
|
20
|
-
const root =
|
|
21
|
-
|
|
22
|
-
return files.stdout.trim().split("\n");
|
|
48
|
+
const root = await getVolumeMountpoint(name);
|
|
49
|
+
return await readFile(join(root, path), 'utf8');
|
|
23
50
|
}
|
|
24
51
|
async function show(options) {
|
|
25
52
|
if (!isValidName(options.name)) {
|
|
26
53
|
throw invalidNameError;
|
|
27
54
|
}
|
|
28
|
-
const output = await exec(
|
|
55
|
+
const output = await exec('docker', ['volume', 'inspect', options.name]);
|
|
29
56
|
const list = JSON.parse(output.stdout);
|
|
30
57
|
if (!list.length) {
|
|
31
|
-
throw new Error(
|
|
58
|
+
throw new Error('Volume not found');
|
|
32
59
|
}
|
|
33
60
|
const { Name, Labels, CreatedAt } = list[0];
|
|
34
61
|
return {
|
|
@@ -41,24 +68,24 @@ async function add(options) {
|
|
|
41
68
|
if (!isValidName(options.name)) {
|
|
42
69
|
throw invalidNameError;
|
|
43
70
|
}
|
|
44
|
-
const output = await exec(
|
|
71
|
+
const output = await exec('docker', ['volume', 'create', options.name]);
|
|
45
72
|
if (output.ok) {
|
|
46
73
|
return true;
|
|
47
74
|
}
|
|
48
|
-
throw new Error(
|
|
75
|
+
throw new Error('Unable to create volume');
|
|
49
76
|
}
|
|
50
77
|
async function remove(options) {
|
|
51
78
|
if (!isValidName(options.name)) {
|
|
52
79
|
throw invalidNameError;
|
|
53
80
|
}
|
|
54
|
-
const output = await exec(
|
|
81
|
+
const output = await exec('docker', ['volume', 'rm', options.name]);
|
|
55
82
|
if (output.ok) {
|
|
56
83
|
return true;
|
|
57
84
|
}
|
|
58
|
-
throw new Error(
|
|
85
|
+
throw new Error('Unable to remove volume');
|
|
59
86
|
}
|
|
60
87
|
async function prune() {
|
|
61
|
-
await exec(
|
|
62
|
-
return
|
|
88
|
+
await exec('docker', ['volume', 'prune']);
|
|
89
|
+
return '';
|
|
63
90
|
}
|
|
64
|
-
export default { add, remove, list, show, prune, ls };
|
|
91
|
+
export default { add, remove, list, show, prune, ls, rm, cat, fixPermissions };
|
package/package.json
CHANGED
package/src/index.spec.ts
CHANGED
|
@@ -2,12 +2,16 @@ const execMocks = {
|
|
|
2
2
|
exec: jest.fn(),
|
|
3
3
|
};
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
5
|
+
const fsMocks = {
|
|
6
|
+
readFile: jest.fn(),
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
jest.mock('@cloud-cli/exec', () => execMocks);
|
|
10
|
+
jest.mock('node:fs/promises', () => fsMocks);
|
|
8
11
|
|
|
9
12
|
import vm from './index';
|
|
10
13
|
import * as exec from '@cloud-cli/exec';
|
|
14
|
+
import * as fs from 'node:fs/promises';
|
|
11
15
|
|
|
12
16
|
const inspectOutput = `[{
|
|
13
17
|
"CreatedAt": "2023-03-10T10:15:10Z",
|
|
@@ -44,6 +48,17 @@ describe('volume manager', () => {
|
|
|
44
48
|
});
|
|
45
49
|
});
|
|
46
50
|
|
|
51
|
+
describe('fixPermissions', () => {
|
|
52
|
+
it('should fix write permissions on volume folder', async () => {
|
|
53
|
+
const outputs = [inspectOutput, ''];
|
|
54
|
+
execMocks.exec = jest.fn().mockImplementation(() => ({ ok: true, stdout: outputs.shift() }));
|
|
55
|
+
|
|
56
|
+
await expect(vm.fixPermissions({ name: 'test' })).resolves.toEqual(true);
|
|
57
|
+
expect(exec.exec).toHaveBeenCalledWith('docker', ['volume', 'inspect', 'test']);
|
|
58
|
+
expect(exec.exec).toHaveBeenCalledWith('chmod', ['-R', 'a+w', '/var/lib/docker/volumes/test/_data']);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
47
62
|
describe('ls', () => {
|
|
48
63
|
it('should list files of a volume and path', async () => {
|
|
49
64
|
const outputs = [inspectOutput, 'dir', inspectOutput, 'a.txt\nb.txt'];
|
|
@@ -63,6 +78,52 @@ describe('volume manager', () => {
|
|
|
63
78
|
});
|
|
64
79
|
});
|
|
65
80
|
|
|
81
|
+
describe('rm', () => {
|
|
82
|
+
it('should remove files of a volume by path', async () => {
|
|
83
|
+
const outputs = [inspectOutput, ''];
|
|
84
|
+
execMocks.exec = jest.fn().mockImplementation(() => ({ ok: true, stdout: outputs.shift() }));
|
|
85
|
+
|
|
86
|
+
await expect(vm.rm({ name: 'test', path: 'file.txt' })).resolves.toEqual(true);
|
|
87
|
+
|
|
88
|
+
expect(exec.exec).toHaveBeenCalledWith('docker', ['volume', 'inspect', 'test']);
|
|
89
|
+
expect(exec.exec).toHaveBeenCalledWith('rm', ['-r', '/var/lib/docker/volumes/test/_data/file.txt']);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should throw an error if volume name is invalid', async () => {
|
|
93
|
+
execMocks.exec = jest.fn().mockImplementation(() => ({ ok: true, stdout: '[]' }));
|
|
94
|
+
await expect(vm.rm({ name: 'notfound', path: 'file.txt' })).rejects.toThrowError('Volume not found');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should throw an error if path is invalid', async () => {
|
|
98
|
+
execMocks.exec = jest.fn().mockImplementation(() => ({ ok: true, stdout: inspectOutput }));
|
|
99
|
+
await expect(vm.rm({ name: 'nopath' })).rejects.toThrowError('Path not specified');
|
|
100
|
+
expect(execMocks.exec).not.toHaveBeenCalled();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('cat', () => {
|
|
105
|
+
it('should read the content of a file in a volume by path', async () => {
|
|
106
|
+
execMocks.exec = jest.fn().mockImplementation(() => ({ ok: true, stdout: inspectOutput }));
|
|
107
|
+
fsMocks.readFile = jest.fn().mockImplementation(() => 'test file');
|
|
108
|
+
|
|
109
|
+
await expect(vm.cat({ name: 'test', path: 'file.txt' })).resolves.toEqual('test file');
|
|
110
|
+
|
|
111
|
+
expect(exec.exec).toHaveBeenCalledWith('docker', ['volume', 'inspect', 'test']);
|
|
112
|
+
expect(fs.readFile).toHaveBeenCalledWith('/var/lib/docker/volumes/test/_data/file.txt', 'utf8');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should throw an error if volume name is invalid', async () => {
|
|
116
|
+
execMocks.exec = jest.fn().mockImplementation(() => ({ ok: true, stdout: '[]' }));
|
|
117
|
+
await expect(vm.cat({ name: 'notfound', path: 'file.txt' })).rejects.toThrowError('Volume not found');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should throw an error if path is invalid', async () => {
|
|
121
|
+
execMocks.exec = jest.fn().mockImplementation(() => ({ ok: true, stdout: inspectOutput }));
|
|
122
|
+
await expect(vm.cat({ name: 'nopath' })).rejects.toThrowError('Path not specified');
|
|
123
|
+
expect(execMocks.exec).not.toHaveBeenCalled();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
66
127
|
describe('show', () => {
|
|
67
128
|
it('should show details of a volume', async () => {
|
|
68
129
|
const execOutput = { ok: true, stdout: inspectOutput };
|
package/src/index.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { join } from
|
|
2
|
-
import {
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { exec } from '@cloud-cli/exec';
|
|
3
4
|
|
|
4
5
|
const nameRe = /^[a-z][a-z0-9-]+$/;
|
|
5
6
|
const isValidName = (name) => nameRe.test(name);
|
|
6
|
-
const invalidNameError = new Error(
|
|
7
|
+
const invalidNameError = new Error('Invalid name');
|
|
7
8
|
|
|
8
9
|
interface NameOption {
|
|
9
10
|
name: string;
|
|
@@ -15,28 +16,64 @@ interface NameAndPathOption {
|
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
async function list() {
|
|
18
|
-
const output = await exec(
|
|
19
|
+
const output = await exec('docker', ['volume', 'ls', '--format={{.Name}}']);
|
|
19
20
|
|
|
20
21
|
if (!output.ok) {
|
|
21
|
-
throw new Error(
|
|
22
|
+
throw new Error('Unable to list volumes');
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
return output.stdout.trim().split(
|
|
25
|
+
return output.stdout.trim().split('\n');
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
async function
|
|
28
|
-
const
|
|
29
|
-
const output = await exec("docker", ["volume", "inspect", name]);
|
|
28
|
+
async function getVolumeMountpoint(name): Promise<string> {
|
|
29
|
+
const output = await exec('docker', ['volume', 'inspect', name]);
|
|
30
30
|
const volumes = JSON.parse(output.stdout);
|
|
31
31
|
|
|
32
32
|
if (!volumes.length) {
|
|
33
|
-
throw new Error(
|
|
33
|
+
throw new Error('Volume not found');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return volumes[0].Mountpoint;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function ls(options: NameAndPathOption) {
|
|
40
|
+
const { name, path = '' } = options;
|
|
41
|
+
const root = await getVolumeMountpoint(name);
|
|
42
|
+
const files = await exec('ls', ['-1', join(root, path)]);
|
|
43
|
+
|
|
44
|
+
return files.stdout.trim().split('\n');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function fixPermissions(options: NameOption) {
|
|
48
|
+
const { name } = options;
|
|
49
|
+
const root = await getVolumeMountpoint(name);
|
|
50
|
+
const result = await exec('chmod', ['-R', 'a+w', root]);
|
|
51
|
+
|
|
52
|
+
return result.ok;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function rm(options: NameAndPathOption) {
|
|
56
|
+
const { name, path } = options;
|
|
57
|
+
|
|
58
|
+
if (!path) {
|
|
59
|
+
throw new Error('Path not specified');
|
|
34
60
|
}
|
|
35
61
|
|
|
36
|
-
const root =
|
|
37
|
-
const
|
|
62
|
+
const root = await getVolumeMountpoint(name);
|
|
63
|
+
const result = await exec('rm', ['-r', join(root, path)]);
|
|
64
|
+
|
|
65
|
+
return result.ok;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function cat(options: NameAndPathOption) {
|
|
69
|
+
const { name, path } = options;
|
|
70
|
+
|
|
71
|
+
if (!path) {
|
|
72
|
+
throw new Error('Path not specified');
|
|
73
|
+
}
|
|
38
74
|
|
|
39
|
-
|
|
75
|
+
const root = await getVolumeMountpoint(name);
|
|
76
|
+
return await readFile(join(root, path), 'utf8');
|
|
40
77
|
}
|
|
41
78
|
|
|
42
79
|
async function show(options: NameOption) {
|
|
@@ -44,11 +81,11 @@ async function show(options: NameOption) {
|
|
|
44
81
|
throw invalidNameError;
|
|
45
82
|
}
|
|
46
83
|
|
|
47
|
-
const output = await exec(
|
|
84
|
+
const output = await exec('docker', ['volume', 'inspect', options.name]);
|
|
48
85
|
const list = JSON.parse(output.stdout);
|
|
49
86
|
|
|
50
87
|
if (!list.length) {
|
|
51
|
-
throw new Error(
|
|
88
|
+
throw new Error('Volume not found');
|
|
52
89
|
}
|
|
53
90
|
|
|
54
91
|
const { Name, Labels, CreatedAt } = list[0];
|
|
@@ -65,13 +102,13 @@ async function add(options: NameOption) {
|
|
|
65
102
|
throw invalidNameError;
|
|
66
103
|
}
|
|
67
104
|
|
|
68
|
-
const output = await exec(
|
|
105
|
+
const output = await exec('docker', ['volume', 'create', options.name]);
|
|
69
106
|
|
|
70
107
|
if (output.ok) {
|
|
71
108
|
return true;
|
|
72
109
|
}
|
|
73
110
|
|
|
74
|
-
throw new Error(
|
|
111
|
+
throw new Error('Unable to create volume');
|
|
75
112
|
}
|
|
76
113
|
|
|
77
114
|
async function remove(options: NameOption) {
|
|
@@ -79,18 +116,18 @@ async function remove(options: NameOption) {
|
|
|
79
116
|
throw invalidNameError;
|
|
80
117
|
}
|
|
81
118
|
|
|
82
|
-
const output = await exec(
|
|
119
|
+
const output = await exec('docker', ['volume', 'rm', options.name]);
|
|
83
120
|
|
|
84
121
|
if (output.ok) {
|
|
85
122
|
return true;
|
|
86
123
|
}
|
|
87
124
|
|
|
88
|
-
throw new Error(
|
|
125
|
+
throw new Error('Unable to remove volume');
|
|
89
126
|
}
|
|
90
127
|
|
|
91
128
|
async function prune() {
|
|
92
|
-
await exec(
|
|
93
|
-
return
|
|
129
|
+
await exec('docker', ['volume', 'prune']);
|
|
130
|
+
return '';
|
|
94
131
|
}
|
|
95
132
|
|
|
96
|
-
export default { add, remove, list, show, prune, ls };
|
|
133
|
+
export default { add, remove, list, show, prune, ls, rm, cat, fixPermissions };
|