Skip to content

Commit e8726b0

Browse files
committed
feat(sandbox): add sandbox module
Add the Qiniu Sandbox SDK surface with instance lifecycle, filesystem, command execution, Git, PTY, template, resource mounting, and request injection APIs. Include TypeScript declarations, examples, CI coverage, and focused sandbox tests.
1 parent 7c22513 commit e8726b0

47 files changed

Lines changed: 10774 additions & 2 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Copy this file to .env in the project root before running sandbox examples
2+
# or sandbox integration tests.
3+
4+
# Sandbox API key auth.
5+
QINIU_SANDBOX_API_KEY=
6+
7+
# Optional custom endpoint.
8+
QINIU_SANDBOX_ENDPOINT=
9+
10+
# Optional template alias or ID. Defaults to base.
11+
QINIU_SANDBOX_TEMPLATE=base
12+
13+
# Required only for injection-rule and Kodo resource examples.
14+
QINIU_SANDBOX_ACCESS_KEY=
15+
QINIU_SANDBOX_SECRET_KEY=
16+
17+
# Optional Git remote examples.
18+
GIT_REPO_URL=
19+
GIT_USERNAME=
20+
GIT_PASSWORD=
21+
22+
# Optional Git repository resource example.
23+
GITHUB_TOKEN=
24+
QINIU_SANDBOX_GIT_MOUNT_PATH=/workspace/repo
25+
26+
# Optional Kodo resource example.
27+
QINIU_SANDBOX_KODO_BUCKET=
28+
QINIU_SANDBOX_KODO_MOUNT_PATH=/workspace/kodo
29+
QINIU_SANDBOX_KODO_PREFIX=
30+
31+
# Optional request injection examples.
32+
QINIU_SANDBOX_HTTP_INJECTION_TOKEN=real_token
33+
QINIU_SANDBOX_OPENAI_API_KEY=

.github/workflows/ci-test.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,12 @@ jobs:
2424
- name: Run cases
2525
run: |
2626
npm run check-type
27-
nyc --reporter=lcov npm test
27+
nyc --reporter=lcov --include 'qiniu/sandbox/**/*.js' npm run test:sandbox
28+
if [ -n "$QINIU_ACCESS_KEY" ] && [ -n "$QINIU_SECRET_KEY" ] && [ -n "$QINIU_TEST_BUCKET" ] && [ -n "$QINIU_TEST_DOMAIN" ]; then
29+
nyc --no-clean --reporter=lcov npm test
30+
else
31+
npm test
32+
fi
2833
env:
2934
QINIU_ACCESS_KEY: ${{ secrets.QINIU_ACCESS_KEY }}
3035
QINIU_SECRET_KEY: ${{ secrets.QINIU_SECRET_KEY }}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@ coverage/
2727
yarn.lock
2828

2929
.claude/settings.local.json
30+
.env

examples/sandbox_common.js

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
4+
const qiniu = require('../index');
5+
const { shellQuote } = require('../qiniu/sandbox/util');
6+
7+
function loadDotEnvIfPresent () {
8+
const files = [
9+
path.join(process.cwd(), '.env')
10+
];
11+
12+
files.forEach(filepath => {
13+
if (!fs.existsSync(filepath)) {
14+
return;
15+
}
16+
17+
fs.readFileSync(filepath, 'utf8')
18+
.split(/\r?\n/)
19+
.forEach(line => {
20+
line = line.trim();
21+
if (!line || line[0] === '#') {
22+
return;
23+
}
24+
const index = line.indexOf('=');
25+
if (index < 0) {
26+
return;
27+
}
28+
const key = line.slice(0, index).trim();
29+
let value = line.slice(index + 1).trim();
30+
if (
31+
(value[0] === '"' && value[value.length - 1] === '"') ||
32+
(value[0] === '\'' && value[value.length - 1] === '\'')
33+
) {
34+
value = value.slice(1, -1);
35+
}
36+
if (process.env[key] === undefined) {
37+
process.env[key] = value;
38+
}
39+
});
40+
});
41+
}
42+
43+
function env (key, fallback) {
44+
return process.env[key] || fallback;
45+
}
46+
47+
function requiredEnv (key) {
48+
if (process.env[key]) {
49+
return process.env[key];
50+
}
51+
throw new Error(`Please set ${key}`);
52+
}
53+
54+
function sandboxEndpoint () {
55+
return env('QINIU_SANDBOX_ENDPOINT');
56+
}
57+
58+
function sandboxApiKey () {
59+
return requiredEnv('QINIU_SANDBOX_API_KEY');
60+
}
61+
62+
function sandboxTemplate () {
63+
return env('QINIU_SANDBOX_TEMPLATE', 'base');
64+
}
65+
66+
function sandboxClient (options) {
67+
options = Object.assign({
68+
endpoint: sandboxEndpoint(),
69+
apiKey: sandboxApiKey()
70+
}, options || {});
71+
return new qiniu.sandbox.SandboxClient(options);
72+
}
73+
74+
function sandboxMac () {
75+
const accessKey = requiredEnv('QINIU_SANDBOX_ACCESS_KEY');
76+
const secretKey = requiredEnv('QINIU_SANDBOX_SECRET_KEY');
77+
return new qiniu.auth.digest.Mac(accessKey, secretKey);
78+
}
79+
80+
function createSandboxAndWait (options, pollOptions) {
81+
const client = options && options.client ? options.client : sandboxClient();
82+
const params = Object.assign({
83+
client,
84+
template: sandboxTemplate(),
85+
timeout: 300
86+
}, options || {});
87+
88+
return qiniu.sandbox.Sandbox.create(params).then(sandbox => {
89+
console.log('Sandbox created:', sandbox.sandboxId);
90+
return sandbox.waitForReady(Object.assign({
91+
interval: 3000,
92+
timeout: 180000
93+
}, pollOptions || {})).then(info => {
94+
console.log('Sandbox ready:', sandbox.sandboxId, info.state || '');
95+
return sandbox;
96+
});
97+
});
98+
}
99+
100+
function cleanupSandbox (sandbox) {
101+
if (!sandbox) {
102+
return Promise.resolve();
103+
}
104+
return sandbox.kill()
105+
.then(() => {
106+
console.log('Sandbox killed:', sandbox.sandboxId);
107+
}, err => {
108+
console.log('Failed to kill sandbox:', sandbox.sandboxId, err.message);
109+
});
110+
}
111+
112+
function runExample (fn) {
113+
loadDotEnvIfPresent();
114+
Promise.resolve()
115+
.then(fn)
116+
.catch(err => {
117+
console.error(err && err.stack ? err.stack : err);
118+
process.exitCode = 1;
119+
});
120+
}
121+
122+
module.exports = {
123+
qiniu,
124+
shellQuote,
125+
env,
126+
requiredEnv,
127+
sandboxEndpoint,
128+
sandboxApiKey,
129+
sandboxTemplate,
130+
sandboxClient,
131+
sandboxMac,
132+
createSandboxAndWait,
133+
cleanupSandbox,
134+
runExample
135+
};

examples/sandbox_create.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
const {
2+
qiniu,
3+
sandboxClient,
4+
sandboxTemplate,
5+
cleanupSandbox,
6+
runExample
7+
} = require('./sandbox_common');
8+
9+
runExample(() => {
10+
const client = sandboxClient();
11+
let sandbox;
12+
13+
return qiniu.sandbox.Sandbox.create(sandboxTemplate(), {
14+
client,
15+
timeout: 300,
16+
metadata: {
17+
example: 'sandbox_create'
18+
}
19+
}).then(created => {
20+
sandbox = created;
21+
console.log('Sandbox created:', sandbox.sandboxId);
22+
console.log('Template:', sandbox.info.templateID || sandbox.info.template_id || sandboxTemplate());
23+
return cleanupSandbox(sandbox);
24+
});
25+
});

examples/sandbox_envd.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
const {
2+
shellQuote,
3+
createSandboxAndWait,
4+
cleanupSandbox,
5+
runExample
6+
} = require('./sandbox_common');
7+
8+
runExample(() => {
9+
let sandbox;
10+
const workdir = '/tmp/qiniu-nodejs-sdk-envd';
11+
const filePath = `${workdir}/hello.txt`;
12+
13+
return createSandboxAndWait({
14+
metadata: {
15+
example: 'sandbox_envd'
16+
}
17+
}).then(created => {
18+
sandbox = created;
19+
console.log('Public host for port 8080:', sandbox.getHost(8080));
20+
return sandbox.commands.run(`mkdir -p ${shellQuote(workdir)}`);
21+
}).then(result => {
22+
console.log('mkdir exit:', result.exitCode);
23+
return sandbox.files.write(filePath, 'Hello from Qiniu Node.js SDK\n');
24+
}).then(entry => {
25+
console.log('Wrote:', entry.path || entry);
26+
return sandbox.files.readText(filePath);
27+
}).then(text => {
28+
console.log('ReadText:', JSON.stringify(text));
29+
return sandbox.files.writeFiles([
30+
{ path: `${workdir}/batch-a.txt`, data: 'file A content\n' },
31+
{ path: `${workdir}/batch-b.txt`, data: 'file B content\n' }
32+
]);
33+
}).then(entries => {
34+
console.log('WriteFiles:', entries.map(item => item.path));
35+
return sandbox.files.read(`${workdir}/batch-a.txt`, { format: 'bytes' });
36+
}).then(bytes => {
37+
console.log('Read bytes:', bytes.length);
38+
return sandbox.files.list(workdir);
39+
}).then(entries => {
40+
console.log('List:', entries.map(item => `${item.type}:${item.name}`));
41+
return sandbox.files.exists(filePath);
42+
}).then(exists => {
43+
console.log('Exists:', exists);
44+
return sandbox.files.getInfo(filePath);
45+
}).then(info => {
46+
console.log('GetInfo:', info.name, info.size);
47+
return sandbox.files.rename(`${workdir}/batch-b.txt`, `${workdir}/batch-b-renamed.txt`);
48+
}).then(info => {
49+
console.log('Renamed to:', info.path);
50+
return sandbox.files.remove(`${workdir}/batch-b-renamed.txt`);
51+
}).then(() => {
52+
console.log('Removed renamed file');
53+
return sandbox.commands.run('echo $MY_VAR && pwd', {
54+
cwd: workdir,
55+
envs: {
56+
MY_VAR: 'sandbox-value'
57+
}
58+
});
59+
}).then(result => {
60+
console.log('Command exit:', result.exitCode);
61+
console.log('stdout:\n' + result.stdout);
62+
return cleanupSandbox(sandbox);
63+
}, err => {
64+
return cleanupSandbox(sandbox).then(() => {
65+
throw err;
66+
});
67+
});
68+
});
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
const {
2+
createSandboxAndWait,
3+
cleanupSandbox,
4+
runExample
5+
} = require('./sandbox_common');
6+
7+
runExample(() => {
8+
let sandbox;
9+
const filePath = '/tmp/qiniu-nodejs-sdk-encoding.txt';
10+
11+
return createSandboxAndWait({
12+
metadata: {
13+
example: 'sandbox_filesystem_encoding'
14+
}
15+
}).then(created => {
16+
sandbox = created;
17+
return sandbox.files.write(filePath, 'hello with octet-stream and gzip\n', {
18+
useOctetStream: true,
19+
gzip: true
20+
});
21+
}).then(entry => {
22+
console.log('Wrote:', entry.path || entry);
23+
return sandbox.files.read(filePath, {
24+
gzip: true
25+
});
26+
}).then(text => {
27+
console.log('Read:', JSON.stringify(text));
28+
return cleanupSandbox(sandbox);
29+
}, err => {
30+
return cleanupSandbox(sandbox).then(() => {
31+
throw err;
32+
});
33+
});
34+
});

0 commit comments

Comments
 (0)