@cloud-cli/vm 1.1.0 → 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 CHANGED
@@ -7,6 +7,8 @@ interface NameAndPathOption {
7
7
  }
8
8
  declare function list(): Promise<string[]>;
9
9
  declare function ls(options: NameAndPathOption): Promise<string[]>;
10
+ declare function rm(options: NameAndPathOption): Promise<boolean>;
11
+ declare function cat(options: NameAndPathOption): Promise<string>;
10
12
  declare function show(options: NameOption): Promise<{
11
13
  name: any;
12
14
  createdAt: any;
@@ -22,5 +24,7 @@ declare const _default: {
22
24
  show: typeof show;
23
25
  prune: typeof prune;
24
26
  ls: typeof ls;
27
+ rm: typeof rm;
28
+ cat: typeof cat;
25
29
  };
26
30
  export default _default;
package/dist/index.js CHANGED
@@ -1,34 +1,55 @@
1
- import { join } from "node:path";
2
- import { exec } from "@cloud-cli/exec";
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("Invalid name");
6
+ const invalidNameError = new Error('Invalid name');
6
7
  async function list() {
7
- const output = await exec("docker", ["volume", "ls", "--format={{.Name}}"]);
8
+ const output = await exec('docker', ['volume', 'ls', '--format={{.Name}}']);
8
9
  if (!output.ok) {
9
- throw new Error("Unable to list volumes");
10
+ throw new Error('Unable to list volumes');
10
11
  }
11
- return output.stdout.trim().split("\n");
12
+ return output.stdout.trim().split('\n');
12
13
  }
13
- async function ls(options) {
14
- const { name, path = "" } = options;
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("Volume not found");
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');
19
41
  }
20
- const root = volumes[0].Mountpoint;
21
- const files = await exec("ls", ["-1", join(root, path)]);
22
- return files.stdout.trim().split("\n");
42
+ const root = await getVolumeMountpoint(name);
43
+ return await readFile(join(root, path), 'utf8');
23
44
  }
24
45
  async function show(options) {
25
46
  if (!isValidName(options.name)) {
26
47
  throw invalidNameError;
27
48
  }
28
- const output = await exec("docker", ["volume", "inspect", options.name]);
49
+ const output = await exec('docker', ['volume', 'inspect', options.name]);
29
50
  const list = JSON.parse(output.stdout);
30
51
  if (!list.length) {
31
- throw new Error("Volume not found");
52
+ throw new Error('Volume not found');
32
53
  }
33
54
  const { Name, Labels, CreatedAt } = list[0];
34
55
  return {
@@ -41,24 +62,24 @@ async function add(options) {
41
62
  if (!isValidName(options.name)) {
42
63
  throw invalidNameError;
43
64
  }
44
- const output = await exec("docker", ["volume", "create", options.name]);
65
+ const output = await exec('docker', ['volume', 'create', options.name]);
45
66
  if (output.ok) {
46
67
  return true;
47
68
  }
48
- throw new Error("Unable to create volume");
69
+ throw new Error('Unable to create volume');
49
70
  }
50
71
  async function remove(options) {
51
72
  if (!isValidName(options.name)) {
52
73
  throw invalidNameError;
53
74
  }
54
- const output = await exec("docker", ["volume", "rm", options.name]);
75
+ const output = await exec('docker', ['volume', 'rm', options.name]);
55
76
  if (output.ok) {
56
77
  return true;
57
78
  }
58
- throw new Error("Unable to remove volume");
79
+ throw new Error('Unable to remove volume');
59
80
  }
60
81
  async function prune() {
61
- await exec("docker", ["volume", "prune"]);
62
- return "";
82
+ await exec('docker', ['volume', 'prune']);
83
+ return '';
63
84
  }
64
- export default { add, remove, list, show, prune, ls };
85
+ export default { add, remove, list, show, prune, ls, rm, cat };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloud-cli/vm",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Docker volume management",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/index.spec.ts CHANGED
@@ -2,12 +2,16 @@ const execMocks = {
2
2
  exec: jest.fn(),
3
3
  };
4
4
 
5
- jest.mock('@cloud-cli/exec', () => {
6
- return execMocks;
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",
@@ -63,6 +67,52 @@ describe('volume manager', () => {
63
67
  });
64
68
  });
65
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
+
66
116
  describe('show', () => {
67
117
  it('should show details of a volume', async () => {
68
118
  const execOutput = { ok: true, stdout: inspectOutput };
package/src/index.ts CHANGED
@@ -1,9 +1,10 @@
1
- import { join } from "node:path";
2
- import { exec } from "@cloud-cli/exec";
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("Invalid name");
7
+ const invalidNameError = new Error('Invalid name');
7
8
 
8
9
  interface NameOption {
9
10
  name: string;
@@ -15,28 +16,56 @@ interface NameAndPathOption {
15
16
  }
16
17
 
17
18
  async function list() {
18
- const output = await exec("docker", ["volume", "ls", "--format={{.Name}}"]);
19
+ const output = await exec('docker', ['volume', 'ls', '--format={{.Name}}']);
19
20
 
20
21
  if (!output.ok) {
21
- throw new Error("Unable to list volumes");
22
+ throw new Error('Unable to list volumes');
22
23
  }
23
24
 
24
- return output.stdout.trim().split("\n");
25
+ return output.stdout.trim().split('\n');
25
26
  }
26
27
 
27
- async function ls(options: NameAndPathOption) {
28
- const { name, path = "" } = options;
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("Volume not found");
33
+ throw new Error('Volume not found');
34
34
  }
35
35
 
36
- const root = volumes[0].Mountpoint;
37
- const files = await exec("ls", ["-1", join(root, path)]);
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
+ }
38
66
 
39
- return files.stdout.trim().split("\n");
67
+ const root = await getVolumeMountpoint(name);
68
+ return await readFile(join(root, path), 'utf8');
40
69
  }
41
70
 
42
71
  async function show(options: NameOption) {
@@ -44,11 +73,11 @@ async function show(options: NameOption) {
44
73
  throw invalidNameError;
45
74
  }
46
75
 
47
- const output = await exec("docker", ["volume", "inspect", options.name]);
76
+ const output = await exec('docker', ['volume', 'inspect', options.name]);
48
77
  const list = JSON.parse(output.stdout);
49
78
 
50
79
  if (!list.length) {
51
- throw new Error("Volume not found");
80
+ throw new Error('Volume not found');
52
81
  }
53
82
 
54
83
  const { Name, Labels, CreatedAt } = list[0];
@@ -65,13 +94,13 @@ async function add(options: NameOption) {
65
94
  throw invalidNameError;
66
95
  }
67
96
 
68
- const output = await exec("docker", ["volume", "create", options.name]);
97
+ const output = await exec('docker', ['volume', 'create', options.name]);
69
98
 
70
99
  if (output.ok) {
71
100
  return true;
72
101
  }
73
102
 
74
- throw new Error("Unable to create volume");
103
+ throw new Error('Unable to create volume');
75
104
  }
76
105
 
77
106
  async function remove(options: NameOption) {
@@ -79,18 +108,18 @@ async function remove(options: NameOption) {
79
108
  throw invalidNameError;
80
109
  }
81
110
 
82
- const output = await exec("docker", ["volume", "rm", options.name]);
111
+ const output = await exec('docker', ['volume', 'rm', options.name]);
83
112
 
84
113
  if (output.ok) {
85
114
  return true;
86
115
  }
87
116
 
88
- throw new Error("Unable to remove volume");
117
+ throw new Error('Unable to remove volume');
89
118
  }
90
119
 
91
120
  async function prune() {
92
- await exec("docker", ["volume", "prune"]);
93
- return "";
121
+ await exec('docker', ['volume', 'prune']);
122
+ return '';
94
123
  }
95
124
 
96
- export default { add, remove, list, show, prune, ls };
125
+ export default { add, remove, list, show, prune, ls, rm, cat };