@cloud-cli/vm 1.0.3 → 1.2.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 +12 -2
- package/dist/index.js +36 -3
- package/package.json +1 -1
- package/src/index.spec.ts +76 -7
- package/src/index.ts +53 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
interface NameOption {
|
|
2
2
|
name: string;
|
|
3
3
|
}
|
|
4
|
+
interface NameAndPathOption {
|
|
5
|
+
name: string;
|
|
6
|
+
path?: string;
|
|
7
|
+
}
|
|
4
8
|
declare function list(): Promise<string[]>;
|
|
9
|
+
declare function ls(options: NameAndPathOption): Promise<string[]>;
|
|
10
|
+
declare function rm(options: NameAndPathOption): Promise<boolean>;
|
|
11
|
+
declare function cat(options: NameAndPathOption): Promise<string>;
|
|
5
12
|
declare function show(options: NameOption): Promise<{
|
|
6
13
|
name: any;
|
|
7
14
|
createdAt: any;
|
|
8
15
|
labels: any;
|
|
9
16
|
}>;
|
|
10
|
-
declare function add(options: NameOption): Promise<
|
|
11
|
-
declare function remove(options: NameOption): Promise<
|
|
17
|
+
declare function add(options: NameOption): Promise<boolean>;
|
|
18
|
+
declare function remove(options: NameOption): Promise<boolean>;
|
|
12
19
|
declare function prune(): Promise<string>;
|
|
13
20
|
declare const _default: {
|
|
14
21
|
add: typeof add;
|
|
@@ -16,5 +23,8 @@ declare const _default: {
|
|
|
16
23
|
list: typeof list;
|
|
17
24
|
show: typeof show;
|
|
18
25
|
prune: typeof prune;
|
|
26
|
+
ls: typeof ls;
|
|
27
|
+
rm: typeof rm;
|
|
28
|
+
cat: typeof cat;
|
|
19
29
|
};
|
|
20
30
|
export default _default;
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
1
3
|
import { exec } from '@cloud-cli/exec';
|
|
2
4
|
const nameRe = /^[a-z][a-z0-9-]+$/;
|
|
3
5
|
const isValidName = (name) => nameRe.test(name);
|
|
@@ -9,6 +11,37 @@ async function list() {
|
|
|
9
11
|
}
|
|
10
12
|
return output.stdout.trim().split('\n');
|
|
11
13
|
}
|
|
14
|
+
async function getVolumeMountpoint(name) {
|
|
15
|
+
const output = await exec('docker', ['volume', 'inspect', name]);
|
|
16
|
+
const volumes = JSON.parse(output.stdout);
|
|
17
|
+
if (!volumes.length) {
|
|
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 rm(options) {
|
|
29
|
+
const { name, path } = options;
|
|
30
|
+
if (!path) {
|
|
31
|
+
throw new Error('Path not specified');
|
|
32
|
+
}
|
|
33
|
+
const root = await getVolumeMountpoint(name);
|
|
34
|
+
const result = await exec('rm', ['-r', join(root, path)]);
|
|
35
|
+
return result.ok;
|
|
36
|
+
}
|
|
37
|
+
async function cat(options) {
|
|
38
|
+
const { name, path } = options;
|
|
39
|
+
if (!path) {
|
|
40
|
+
throw new Error('Path not specified');
|
|
41
|
+
}
|
|
42
|
+
const root = await getVolumeMountpoint(name);
|
|
43
|
+
return await readFile(join(root, path), 'utf8');
|
|
44
|
+
}
|
|
12
45
|
async function show(options) {
|
|
13
46
|
if (!isValidName(options.name)) {
|
|
14
47
|
throw invalidNameError;
|
|
@@ -31,7 +64,7 @@ async function add(options) {
|
|
|
31
64
|
}
|
|
32
65
|
const output = await exec('docker', ['volume', 'create', options.name]);
|
|
33
66
|
if (output.ok) {
|
|
34
|
-
return
|
|
67
|
+
return true;
|
|
35
68
|
}
|
|
36
69
|
throw new Error('Unable to create volume');
|
|
37
70
|
}
|
|
@@ -41,7 +74,7 @@ async function remove(options) {
|
|
|
41
74
|
}
|
|
42
75
|
const output = await exec('docker', ['volume', 'rm', options.name]);
|
|
43
76
|
if (output.ok) {
|
|
44
|
-
return
|
|
77
|
+
return true;
|
|
45
78
|
}
|
|
46
79
|
throw new Error('Unable to remove volume');
|
|
47
80
|
}
|
|
@@ -49,4 +82,4 @@ async function prune() {
|
|
|
49
82
|
await exec('docker', ['volume', 'prune']);
|
|
50
83
|
return '';
|
|
51
84
|
}
|
|
52
|
-
export default { add, remove, list, show, prune };
|
|
85
|
+
export default { add, remove, list, show, prune, ls, rm, cat };
|
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,71 @@ describe('volume manager', () => {
|
|
|
44
48
|
});
|
|
45
49
|
});
|
|
46
50
|
|
|
51
|
+
describe('ls', () => {
|
|
52
|
+
it('should list files of a volume and path', async () => {
|
|
53
|
+
const outputs = [inspectOutput, 'dir', inspectOutput, 'a.txt\nb.txt'];
|
|
54
|
+
execMocks.exec = jest.fn().mockImplementation(() => ({ ok: true, stdout: outputs.shift() }));
|
|
55
|
+
|
|
56
|
+
await expect(vm.ls({ name: 'test' })).resolves.toEqual(['dir']);
|
|
57
|
+
await expect(vm.ls({ name: 'test', path: 'dir' })).resolves.toEqual(['a.txt', 'b.txt']);
|
|
58
|
+
|
|
59
|
+
expect(exec.exec).toHaveBeenCalledWith('docker', ['volume', 'inspect', 'test']);
|
|
60
|
+
expect(exec.exec).toHaveBeenCalledWith('ls', ['-1', '/var/lib/docker/volumes/test/_data']);
|
|
61
|
+
expect(exec.exec).toHaveBeenCalledWith('ls', ['-1', '/var/lib/docker/volumes/test/_data/dir']);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should throw an error if volume name is invalid', async () => {
|
|
65
|
+
execMocks.exec = jest.fn().mockImplementation(() => ({ ok: true, stdout: '[]' }));
|
|
66
|
+
await expect(vm.ls({ name: 'notfound' })).rejects.toThrowError('Volume not found');
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('rm', () => {
|
|
71
|
+
it('should remove files of a volume by path', async () => {
|
|
72
|
+
const outputs = [inspectOutput, ''];
|
|
73
|
+
execMocks.exec = jest.fn().mockImplementation(() => ({ ok: true, stdout: outputs.shift() }));
|
|
74
|
+
|
|
75
|
+
await expect(vm.rm({ name: 'test', path: 'file.txt' })).resolves.toEqual(true);
|
|
76
|
+
|
|
77
|
+
expect(exec.exec).toHaveBeenCalledWith('docker', ['volume', 'inspect', 'test']);
|
|
78
|
+
expect(exec.exec).toHaveBeenCalledWith('rm', ['-r', '/var/lib/docker/volumes/test/_data/file.txt']);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should throw an error if volume name is invalid', async () => {
|
|
82
|
+
execMocks.exec = jest.fn().mockImplementation(() => ({ ok: true, stdout: '[]' }));
|
|
83
|
+
await expect(vm.rm({ name: 'notfound', path: 'file.txt' })).rejects.toThrowError('Volume not found');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should throw an error if path is invalid', async () => {
|
|
87
|
+
execMocks.exec = jest.fn().mockImplementation(() => ({ ok: true, stdout: inspectOutput }));
|
|
88
|
+
await expect(vm.rm({ name: 'nopath' })).rejects.toThrowError('Path not specified');
|
|
89
|
+
expect(execMocks.exec).not.toHaveBeenCalled();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('cat', () => {
|
|
94
|
+
it('should read the content of a file in a volume by path', async () => {
|
|
95
|
+
execMocks.exec = jest.fn().mockImplementation(() => ({ ok: true, stdout: inspectOutput }));
|
|
96
|
+
fsMocks.readFile = jest.fn().mockImplementation(() => 'test file');
|
|
97
|
+
|
|
98
|
+
await expect(vm.cat({ name: 'test', path: 'file.txt' })).resolves.toEqual('test file');
|
|
99
|
+
|
|
100
|
+
expect(exec.exec).toHaveBeenCalledWith('docker', ['volume', 'inspect', 'test']);
|
|
101
|
+
expect(fs.readFile).toHaveBeenCalledWith('/var/lib/docker/volumes/test/_data/file.txt', 'utf8');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should throw an error if volume name is invalid', async () => {
|
|
105
|
+
execMocks.exec = jest.fn().mockImplementation(() => ({ ok: true, stdout: '[]' }));
|
|
106
|
+
await expect(vm.cat({ name: 'notfound', path: 'file.txt' })).rejects.toThrowError('Volume not found');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should throw an error if path is invalid', async () => {
|
|
110
|
+
execMocks.exec = jest.fn().mockImplementation(() => ({ ok: true, stdout: inspectOutput }));
|
|
111
|
+
await expect(vm.cat({ name: 'nopath' })).rejects.toThrowError('Path not specified');
|
|
112
|
+
expect(execMocks.exec).not.toHaveBeenCalled();
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
47
116
|
describe('show', () => {
|
|
48
117
|
it('should show details of a volume', async () => {
|
|
49
118
|
const execOutput = { ok: true, stdout: inspectOutput };
|
|
@@ -61,7 +130,7 @@ describe('volume manager', () => {
|
|
|
61
130
|
expect(exec.exec).toHaveBeenCalledWith('docker', ['volume', 'inspect', 'test']);
|
|
62
131
|
});
|
|
63
132
|
|
|
64
|
-
it('should throw error if volume name is invalid', async () => {
|
|
133
|
+
it('should throw an error if volume name is invalid', async () => {
|
|
65
134
|
execMocks.exec = jest.fn();
|
|
66
135
|
await expect(vm.show({ name: 'In$%valid' })).rejects.toThrowError('Invalid name');
|
|
67
136
|
expect(exec.exec).not.toHaveBeenCalled();
|
|
@@ -80,7 +149,7 @@ describe('volume manager', () => {
|
|
|
80
149
|
const execOutput = { ok: true, stdout: 'test' };
|
|
81
150
|
execMocks.exec = jest.fn().mockResolvedValue(execOutput);
|
|
82
151
|
|
|
83
|
-
await expect(vm.add({ name: 'test' })).resolves.toEqual(
|
|
152
|
+
await expect(vm.add({ name: 'test' })).resolves.toEqual(true);
|
|
84
153
|
|
|
85
154
|
expect(exec.exec).toHaveBeenCalledWith('docker', ['volume', 'create', 'test']);
|
|
86
155
|
});
|
|
@@ -99,12 +168,12 @@ describe('volume manager', () => {
|
|
|
99
168
|
});
|
|
100
169
|
});
|
|
101
170
|
|
|
102
|
-
describe('
|
|
171
|
+
describe('remove', () => {
|
|
103
172
|
it('should add a volume', async () => {
|
|
104
173
|
const execOutput = { ok: true, stdout: '' };
|
|
105
174
|
execMocks.exec = jest.fn().mockResolvedValue(execOutput);
|
|
106
175
|
|
|
107
|
-
await expect(vm.remove({ name: 'test' })).resolves.toEqual(
|
|
176
|
+
await expect(vm.remove({ name: 'test' })).resolves.toEqual(true);
|
|
108
177
|
|
|
109
178
|
expect(exec.exec).toHaveBeenCalledWith('docker', ['volume', 'rm', 'test']);
|
|
110
179
|
});
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
1
3
|
import { exec } from '@cloud-cli/exec';
|
|
2
4
|
|
|
3
5
|
const nameRe = /^[a-z][a-z0-9-]+$/;
|
|
@@ -8,6 +10,11 @@ interface NameOption {
|
|
|
8
10
|
name: string;
|
|
9
11
|
}
|
|
10
12
|
|
|
13
|
+
interface NameAndPathOption {
|
|
14
|
+
name: string;
|
|
15
|
+
path?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
11
18
|
async function list() {
|
|
12
19
|
const output = await exec('docker', ['volume', 'ls', '--format={{.Name}}']);
|
|
13
20
|
|
|
@@ -18,6 +25,49 @@ async function list() {
|
|
|
18
25
|
return output.stdout.trim().split('\n');
|
|
19
26
|
}
|
|
20
27
|
|
|
28
|
+
async function getVolumeMountpoint(name): Promise<string> {
|
|
29
|
+
const output = await exec('docker', ['volume', 'inspect', name]);
|
|
30
|
+
const volumes = JSON.parse(output.stdout);
|
|
31
|
+
|
|
32
|
+
if (!volumes.length) {
|
|
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 rm(options: NameAndPathOption) {
|
|
48
|
+
const { name, path } = options;
|
|
49
|
+
|
|
50
|
+
if (!path) {
|
|
51
|
+
throw new Error('Path not specified');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const root = await getVolumeMountpoint(name);
|
|
55
|
+
const result = await exec('rm', ['-r', join(root, path)]);
|
|
56
|
+
|
|
57
|
+
return result.ok;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function cat(options: NameAndPathOption) {
|
|
61
|
+
const { name, path } = options;
|
|
62
|
+
|
|
63
|
+
if (!path) {
|
|
64
|
+
throw new Error('Path not specified');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const root = await getVolumeMountpoint(name);
|
|
68
|
+
return await readFile(join(root, path), 'utf8');
|
|
69
|
+
}
|
|
70
|
+
|
|
21
71
|
async function show(options: NameOption) {
|
|
22
72
|
if (!isValidName(options.name)) {
|
|
23
73
|
throw invalidNameError;
|
|
@@ -47,7 +97,7 @@ async function add(options: NameOption) {
|
|
|
47
97
|
const output = await exec('docker', ['volume', 'create', options.name]);
|
|
48
98
|
|
|
49
99
|
if (output.ok) {
|
|
50
|
-
return
|
|
100
|
+
return true;
|
|
51
101
|
}
|
|
52
102
|
|
|
53
103
|
throw new Error('Unable to create volume');
|
|
@@ -61,7 +111,7 @@ async function remove(options: NameOption) {
|
|
|
61
111
|
const output = await exec('docker', ['volume', 'rm', options.name]);
|
|
62
112
|
|
|
63
113
|
if (output.ok) {
|
|
64
|
-
return
|
|
114
|
+
return true;
|
|
65
115
|
}
|
|
66
116
|
|
|
67
117
|
throw new Error('Unable to remove volume');
|
|
@@ -72,4 +122,4 @@ async function prune() {
|
|
|
72
122
|
return '';
|
|
73
123
|
}
|
|
74
124
|
|
|
75
|
-
export default { add, remove, list, show, prune };
|
|
125
|
+
export default { add, remove, list, show, prune, ls, rm, cat };
|