@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 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 "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 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 = volumes[0].Mountpoint;
21
- const files = await exec("ls", ["-1", join(root, path)]);
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("docker", ["volume", "inspect", options.name]);
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("Volume not found");
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("docker", ["volume", "create", options.name]);
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("Unable to create volume");
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("docker", ["volume", "rm", options.name]);
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("Unable to remove volume");
85
+ throw new Error('Unable to remove volume');
59
86
  }
60
87
  async function prune() {
61
- await exec("docker", ["volume", "prune"]);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloud-cli/vm",
3
- "version": "1.1.0",
3
+ "version": "1.3.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",
@@ -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 "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,64 @@ 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
+ }
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 = volumes[0].Mountpoint;
37
- const files = await exec("ls", ["-1", join(root, path)]);
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
- return files.stdout.trim().split("\n");
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("docker", ["volume", "inspect", options.name]);
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("Volume not found");
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("docker", ["volume", "create", options.name]);
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("Unable to create volume");
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("docker", ["volume", "rm", options.name]);
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("Unable to remove volume");
125
+ throw new Error('Unable to remove volume');
89
126
  }
90
127
 
91
128
  async function prune() {
92
- await exec("docker", ["volume", "prune"]);
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 };