@ferocia-oss/loki-target-chrome-docker 0.31.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.
Potentially problematic release.
This version of @ferocia-oss/loki-target-chrome-docker might be problematic. Click here for more details.
- package/LICENSE +22 -0
- package/package.json +36 -0
- package/src/create-chrome-docker-target.js +229 -0
- package/src/create-chrome-docker-target.spec.js +128 -0
- package/src/docker-seccomp.json +1535 -0
- package/src/get-local-ip-address.js +15 -0
- package/src/get-network-host.js +31 -0
- package/src/index.js +3 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2017 Joel Arvidsson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ferocia-oss/loki-target-chrome-docker",
|
|
3
|
+
"version": "0.31.0",
|
|
4
|
+
"description": "Loki Chrome docker target",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"loki"
|
|
7
|
+
],
|
|
8
|
+
"homepage": "https://github.com/oblador/loki/tree/master/packages/target-chrome-docker",
|
|
9
|
+
"bugs": {
|
|
10
|
+
"url": "https://github.com/oblador/loki/issues"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/oblador/loki.git",
|
|
15
|
+
"directory": "packages/target-chrome-docker"
|
|
16
|
+
},
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"files": [
|
|
19
|
+
"src"
|
|
20
|
+
],
|
|
21
|
+
"main": "src/index.js",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@ferocia-oss/loki-core": "^0.31.0",
|
|
24
|
+
"@ferocia-oss/loki-target-chrome-core": "^0.31.0",
|
|
25
|
+
"chrome-remote-interface": "^0.29.0",
|
|
26
|
+
"debug": "^4.1.1",
|
|
27
|
+
"execa": "^5.0.0",
|
|
28
|
+
"fs-extra": "^9.1.0",
|
|
29
|
+
"get-port": "^5.1.1",
|
|
30
|
+
"wait-on": "^5.2.1"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"gitHead": "d01d38e10afcd368dee3d07311e7a48b155d6a34"
|
|
36
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
const debug = require('debug')('loki:chrome:docker');
|
|
2
|
+
const { execSync } = require('child_process');
|
|
3
|
+
const execa = require('execa');
|
|
4
|
+
const waitOn = require('wait-on');
|
|
5
|
+
const CDP = require('chrome-remote-interface');
|
|
6
|
+
const getRandomPort = require('get-port');
|
|
7
|
+
const {
|
|
8
|
+
ChromeError,
|
|
9
|
+
ensureDependencyAvailable,
|
|
10
|
+
getAbsoluteURL,
|
|
11
|
+
} = require('@ferocia-oss/loki-core');
|
|
12
|
+
const { createChromeTarget } = require('@ferocia-oss/loki-target-chrome-core');
|
|
13
|
+
const { getLocalIPAddress } = require('./get-local-ip-address');
|
|
14
|
+
const { getNetworkHost } = require('./get-network-host');
|
|
15
|
+
|
|
16
|
+
const getExecutor = (dockerWithSudo) => (dockerPath, args) => {
|
|
17
|
+
if (dockerWithSudo) {
|
|
18
|
+
return execa('sudo', [dockerPath, ...args]);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return execa(dockerPath, args);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const waitOnCDPAvailable = (host, port) =>
|
|
25
|
+
new Promise((resolve, reject) => {
|
|
26
|
+
waitOn(
|
|
27
|
+
{
|
|
28
|
+
resources: [`tcp:${host}:${port}`],
|
|
29
|
+
delay: 50,
|
|
30
|
+
interval: 100,
|
|
31
|
+
timeout: 5000,
|
|
32
|
+
},
|
|
33
|
+
(err) => {
|
|
34
|
+
if (err) {
|
|
35
|
+
reject(err);
|
|
36
|
+
} else {
|
|
37
|
+
resolve();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
function createChromeDockerTarget({
|
|
44
|
+
baseUrl = 'http://localhost:6006',
|
|
45
|
+
chromeDockerImage = 'yukinying/chrome-headless-browser-stable',
|
|
46
|
+
chromeFlags = ['--headless', '--disable-gpu', '--hide-scrollbars'],
|
|
47
|
+
dockerNet = null,
|
|
48
|
+
dockerWithSudo = false,
|
|
49
|
+
chromeDockerUseCopy = false,
|
|
50
|
+
chromeDockerWithoutSeccomp = false,
|
|
51
|
+
}) {
|
|
52
|
+
let port;
|
|
53
|
+
let dockerId;
|
|
54
|
+
let host;
|
|
55
|
+
let localPath;
|
|
56
|
+
let dockerUrl = getAbsoluteURL(baseUrl);
|
|
57
|
+
const isLocalFile = dockerUrl.indexOf('file:') === 0;
|
|
58
|
+
const staticMountPath = '/var/loki';
|
|
59
|
+
const dockerPath = 'docker';
|
|
60
|
+
const runArgs = ['run', '--rm', '-d', '-P'];
|
|
61
|
+
const execute = getExecutor(dockerWithSudo);
|
|
62
|
+
|
|
63
|
+
if (!chromeDockerWithoutSeccomp) {
|
|
64
|
+
runArgs.push(`--security-opt=seccomp=${__dirname}/docker-seccomp.json`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (dockerUrl.indexOf('http://localhost') === 0) {
|
|
68
|
+
const ip = getLocalIPAddress();
|
|
69
|
+
if (!ip) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
'Unable to detect local IP address, try passing --host argument'
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
dockerUrl = dockerUrl.replace('localhost', ip);
|
|
75
|
+
} else if (isLocalFile) {
|
|
76
|
+
localPath = dockerUrl.substr('file:'.length);
|
|
77
|
+
dockerUrl = `file://${staticMountPath}`;
|
|
78
|
+
if (!chromeDockerUseCopy) {
|
|
79
|
+
// setup volume mount if we're not using copy
|
|
80
|
+
runArgs.push('-v');
|
|
81
|
+
runArgs.push(`${localPath}:${staticMountPath}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function getIsImageDownloaded(imageName) {
|
|
86
|
+
const { exitCode, stdout, stderr } = await execute(dockerPath, [
|
|
87
|
+
'images',
|
|
88
|
+
'-q',
|
|
89
|
+
imageName,
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
if (exitCode !== 0) {
|
|
93
|
+
throw new Error(`Failed querying docker, ${stderr}`);
|
|
94
|
+
}
|
|
95
|
+
return stdout.trim().length !== 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function copyFiles() {
|
|
99
|
+
const { exitCode, stdout, stderr } = await execute(dockerPath, [
|
|
100
|
+
'cp',
|
|
101
|
+
localPath,
|
|
102
|
+
`${dockerId}:${staticMountPath}`,
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
if (exitCode !== 0) {
|
|
106
|
+
throw new Error(`Failed to copy files, ${stderr}`);
|
|
107
|
+
}
|
|
108
|
+
return stdout.trim().length !== 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function ensureImageDownloaded() {
|
|
112
|
+
ensureDependencyAvailable('docker');
|
|
113
|
+
|
|
114
|
+
const isImageDownloaded = await getIsImageDownloaded(chromeDockerImage);
|
|
115
|
+
if (!isImageDownloaded) {
|
|
116
|
+
await execute(dockerPath, ['pull', chromeDockerImage]);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function start() {
|
|
121
|
+
port = await getRandomPort();
|
|
122
|
+
|
|
123
|
+
ensureDependencyAvailable('docker');
|
|
124
|
+
const dockerArgs = runArgs.concat([
|
|
125
|
+
'--shm-size=1g',
|
|
126
|
+
'-p',
|
|
127
|
+
`${port}:${port}`,
|
|
128
|
+
]);
|
|
129
|
+
|
|
130
|
+
if (dockerNet) {
|
|
131
|
+
dockerArgs.push(`--net=${dockerNet}`);
|
|
132
|
+
}
|
|
133
|
+
dockerArgs.push(chromeDockerImage);
|
|
134
|
+
|
|
135
|
+
const args = dockerArgs
|
|
136
|
+
.concat([
|
|
137
|
+
'--disable-datasaver-prompt',
|
|
138
|
+
'--no-first-run',
|
|
139
|
+
'--disable-extensions',
|
|
140
|
+
'--remote-debugging-address=0.0.0.0',
|
|
141
|
+
`--remote-debugging-port=${port}`,
|
|
142
|
+
])
|
|
143
|
+
.concat(chromeFlags);
|
|
144
|
+
|
|
145
|
+
debug(
|
|
146
|
+
`Launching chrome in docker with command "${dockerPath} ${args.join(
|
|
147
|
+
' '
|
|
148
|
+
)}"`
|
|
149
|
+
);
|
|
150
|
+
const { exitCode, stdout, stderr } = await execute(dockerPath, args);
|
|
151
|
+
if (exitCode === 0) {
|
|
152
|
+
dockerId = stdout;
|
|
153
|
+
if (chromeDockerUseCopy) {
|
|
154
|
+
await copyFiles();
|
|
155
|
+
}
|
|
156
|
+
const logs = execute(dockerPath, ['logs', dockerId, '--follow']);
|
|
157
|
+
const errorLogs = [];
|
|
158
|
+
logs.stderr.on('data', (chunk) => {
|
|
159
|
+
errorLogs.push(chunk);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
host = await getNetworkHost(execute, dockerId);
|
|
163
|
+
try {
|
|
164
|
+
await waitOnCDPAvailable(host, port);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
if (
|
|
167
|
+
error.message.startsWith('Timed out waiting for') &&
|
|
168
|
+
errorLogs.length !== 0
|
|
169
|
+
) {
|
|
170
|
+
throw new ChromeError(
|
|
171
|
+
`Chrome failed to start with ${
|
|
172
|
+
errorLogs.length === 1 ? 'error' : 'errors'
|
|
173
|
+
} ${errorLogs
|
|
174
|
+
.map((e) => `"${e.toString('utf8').trim()}"`)
|
|
175
|
+
.join(', ')}`
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
throw error;
|
|
179
|
+
} finally {
|
|
180
|
+
if (logs.exitCode === null && !logs.killed) {
|
|
181
|
+
logs.kill();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
debug(`Docker started with id ${dockerId}`);
|
|
185
|
+
} else {
|
|
186
|
+
throw new Error(`Failed starting docker, ${stderr}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function stop() {
|
|
191
|
+
if (dockerId) {
|
|
192
|
+
debug(`Killing chrome docker instance with id ${dockerId}`);
|
|
193
|
+
await execute(dockerPath, ['kill', dockerId]);
|
|
194
|
+
} else {
|
|
195
|
+
debug('No chrome docker instance to kill');
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function createNewDebuggerInstance() {
|
|
200
|
+
debug(`Launching new tab with debugger at port ${host}:${port}`);
|
|
201
|
+
const target = await CDP.New({ host, port });
|
|
202
|
+
debug(`Launched with target id ${target.id}`);
|
|
203
|
+
const client = await CDP({ host, port, target });
|
|
204
|
+
|
|
205
|
+
client.close = () => {
|
|
206
|
+
debug('Closing tab');
|
|
207
|
+
return CDP.Close({ host, port, id: target.id });
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
return client;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
process.on('SIGINT', () => {
|
|
214
|
+
if (dockerId) {
|
|
215
|
+
const maybeSudo = dockerWithSudo ? 'sudo ' : '';
|
|
216
|
+
execSync(`${maybeSudo}${dockerPath} kill ${dockerId}`);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
return createChromeTarget(
|
|
221
|
+
start,
|
|
222
|
+
stop,
|
|
223
|
+
createNewDebuggerInstance,
|
|
224
|
+
dockerUrl,
|
|
225
|
+
ensureImageDownloaded
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
module.exports = { createChromeDockerTarget };
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
const { createChromeDockerTarget } = require('.');
|
|
2
|
+
|
|
3
|
+
const DOCKER_TEST_TIMEOUT = 120000;
|
|
4
|
+
|
|
5
|
+
const fetchStorybookUrl = async (baseUrl) => {
|
|
6
|
+
const target = createChromeDockerTarget({ baseUrl });
|
|
7
|
+
await target.start();
|
|
8
|
+
let result;
|
|
9
|
+
try {
|
|
10
|
+
result = await target.getStorybook({ baseUrl });
|
|
11
|
+
} catch (err) {
|
|
12
|
+
result = err;
|
|
13
|
+
}
|
|
14
|
+
await target.stop();
|
|
15
|
+
if (result instanceof Error) {
|
|
16
|
+
throw result;
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const fetchStorybookFixture = async (fixture) =>
|
|
22
|
+
fetchStorybookUrl(`file:${__dirname}/../../../fixtures/storybook-${fixture}`);
|
|
23
|
+
|
|
24
|
+
const storybook = [
|
|
25
|
+
{
|
|
26
|
+
id: 'example-button--large',
|
|
27
|
+
kind: 'Example/Button',
|
|
28
|
+
story: 'Large',
|
|
29
|
+
parameters: {
|
|
30
|
+
args: {
|
|
31
|
+
label: 'Button',
|
|
32
|
+
size: 'large',
|
|
33
|
+
},
|
|
34
|
+
fileName: './src/stories/Button.stories.jsx',
|
|
35
|
+
globals: {
|
|
36
|
+
measureEnabled: false,
|
|
37
|
+
outline: false,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'example-button--primary',
|
|
43
|
+
kind: 'Example/Button',
|
|
44
|
+
story: 'Primary',
|
|
45
|
+
parameters: {
|
|
46
|
+
args: {
|
|
47
|
+
label: 'Button',
|
|
48
|
+
primary: true,
|
|
49
|
+
},
|
|
50
|
+
fileName: './src/stories/Button.stories.jsx',
|
|
51
|
+
globals: {
|
|
52
|
+
measureEnabled: false,
|
|
53
|
+
outline: false,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 'example-button--secondary',
|
|
59
|
+
kind: 'Example/Button',
|
|
60
|
+
story: 'Secondary',
|
|
61
|
+
parameters: {
|
|
62
|
+
args: {
|
|
63
|
+
label: 'Button',
|
|
64
|
+
},
|
|
65
|
+
fileName: './src/stories/Button.stories.jsx',
|
|
66
|
+
globals: {
|
|
67
|
+
measureEnabled: false,
|
|
68
|
+
outline: false,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: 'example-button--small',
|
|
74
|
+
kind: 'Example/Button',
|
|
75
|
+
story: 'Small',
|
|
76
|
+
parameters: {
|
|
77
|
+
args: {
|
|
78
|
+
label: 'Button',
|
|
79
|
+
size: 'small',
|
|
80
|
+
},
|
|
81
|
+
fileName: './src/stories/Button.stories.jsx',
|
|
82
|
+
globals: {
|
|
83
|
+
measureEnabled: false,
|
|
84
|
+
outline: false,
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
describe('createChromeTarget', () => {
|
|
91
|
+
describe('.getStorybook', () => {
|
|
92
|
+
it(
|
|
93
|
+
'fetches stories from webpack dynamic bundles',
|
|
94
|
+
async () => {
|
|
95
|
+
expect(await fetchStorybookFixture('dynamic')).toEqual(storybook);
|
|
96
|
+
},
|
|
97
|
+
DOCKER_TEST_TIMEOUT
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
it(
|
|
101
|
+
'fetches stories from static bundles',
|
|
102
|
+
async () => {
|
|
103
|
+
expect(await fetchStorybookFixture('static')).toEqual(storybook);
|
|
104
|
+
},
|
|
105
|
+
DOCKER_TEST_TIMEOUT
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
it(
|
|
109
|
+
'throws if not configured',
|
|
110
|
+
async () => {
|
|
111
|
+
await expect(fetchStorybookFixture('unconfigured')).rejects.toThrow(
|
|
112
|
+
"Unable to get stories. Try adding `import 'loki/configure-react'` to your .storybook/preview.js file."
|
|
113
|
+
);
|
|
114
|
+
},
|
|
115
|
+
DOCKER_TEST_TIMEOUT
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
it(
|
|
119
|
+
'throws if not running',
|
|
120
|
+
async () => {
|
|
121
|
+
await expect(
|
|
122
|
+
fetchStorybookUrl('http://localhost:23456')
|
|
123
|
+
).rejects.toThrow('Failed fetching stories because the server is down');
|
|
124
|
+
},
|
|
125
|
+
DOCKER_TEST_TIMEOUT
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
});
|