Skip to content

Commit e15e2ba

Browse files
authored
feat(sandbox): sync latest sandbox APIs (#454)
1 parent cc987e9 commit e15e2ba

11 files changed

Lines changed: 311 additions & 16 deletions

examples/sandbox_list_connect.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
const {
22
qiniu,
33
sandboxClient,
4+
sandboxTemplate,
45
cleanupSandbox,
56
runExample
67
} = require('./sandbox_common');
78

89
runExample(() => {
910
const client = sandboxClient();
11+
const template = sandboxTemplate();
1012
let sandbox;
1113

1214
return qiniu.sandbox.Sandbox.create({
1315
client,
16+
template,
1417
timeout: 300,
1518
metadata: {
1619
example: 'sandbox_list_connect'
@@ -20,7 +23,10 @@ runExample(() => {
2023
console.log('Created:', sandbox.sandboxId);
2124
return qiniu.sandbox.Sandbox.list({
2225
client,
23-
limit: 10
26+
limit: 10,
27+
query: {
28+
template: [template]
29+
}
2430
});
2531
}).then(items => {
2632
console.log('List result:', Array.isArray(items) ? items.map(item => item.sandboxId || item.sandboxID) : items);

examples/sandbox_request_injections.js

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,29 @@ const {
77

88
runExample(() => {
99
let sandbox;
10+
const httpInjection = {
11+
type: 'http',
12+
baseUrl: 'https://httpbin.org/bearer',
13+
ifHeaders: {
14+
'X-Sandbox-Example': 'qiniu-nodejs-sdk'
15+
},
16+
headers: {
17+
Authorization: `Bearer ${env('QINIU_SANDBOX_HTTP_INJECTION_TOKEN', 'real_token')}`
18+
}
19+
};
1020

1121
return createSandboxAndWait({
12-
injections: [
13-
{
14-
type: 'http',
15-
baseUrl: 'https://httpbin.org/bearer',
16-
ifHeaders: {
17-
'X-Sandbox-Example': 'qiniu-nodejs-sdk'
18-
},
19-
headers: {
20-
Authorization: `Bearer ${env('QINIU_SANDBOX_HTTP_INJECTION_TOKEN', 'real_token')}`
21-
}
22-
}
23-
],
22+
injections: [httpInjection],
2423
metadata: {
2524
example: 'sandbox_request_injections'
2625
}
2726
}).then(created => {
2827
sandbox = created;
28+
return sandbox.getInjections();
29+
}).then(result => {
30+
console.log('Current runtime injections:', result.injections);
31+
return sandbox.updateInjections([httpInjection]);
32+
}).then(() => {
2933
return sandbox.commands.run('curl --max-time 20 -sSL https://httpbin.org/bearer -H "Authorization: Bearer fake_token" -H "X-Sandbox-Example: qiniu-nodejs-sdk"', {
3034
timeout: 30000
3135
});

examples/sandbox_resources.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ function runGitResourceExample () {
4141
return sandbox.waitForReady({ interval: 3000, timeout: 180000 });
4242
}).then(() => {
4343
console.log('Git resource sandbox ready:', sandbox.sandboxId);
44+
return sandbox.updateGithubToken(token);
45+
}).then(() => {
46+
console.log('GitHub token updated for the running sandbox.');
4447
return sandbox.commands.run(`ls -la ${shellQuote(mountPath)} | head -20`);
4548
}).then(result => {
4649
console.log(result.stdout);

examples/sandbox_templates.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,13 @@ runExample(() => {
2525
}
2626
const id = first.templateID || first.template_id || first.id || sandboxTemplate();
2727
return client.getTemplate(id).then(detail => {
28-
console.log('First template detail:', detail.templateID || detail.template_id || id);
28+
console.log('First template detail:', {
29+
templateID: detail.templateID || detail.template_id || id,
30+
names: detail.names,
31+
aliases: detail.aliases,
32+
isOwner: detail.isOwner,
33+
buildCount: Array.isArray(detail.builds) ? detail.builds.length : 0
34+
});
2935
});
3036
}).then(() => {
3137
const advanced = qiniu.sandbox.Template()

index.d.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,24 @@ export declare namespace sandbox {
130130
type Injection = HttpInjection | OpenaiInjection | AnthropicInjection | GeminiInjection | QiniuInjection | GithubInjection;
131131
type SandboxInjection = InjectionById | InjectionRuleReference | Injection;
132132

133+
interface SandboxInjectionsResponse {
134+
injections: SandboxInjection[];
135+
}
136+
137+
interface TemplateInfo {
138+
templateID: string;
139+
public: boolean;
140+
/** @deprecated use names instead */
141+
aliases: string[];
142+
names: string[];
143+
[key: string]: any;
144+
}
145+
146+
interface TemplateWithBuilds extends TemplateInfo {
147+
isOwner: boolean;
148+
builds: any[];
149+
}
150+
133151
interface InjectionRule {
134152
ruleID?: string;
135153
id?: string;
@@ -456,6 +474,9 @@ export declare namespace sandbox {
456474
getSandboxesMetrics(sandboxIDs: string[] | any): Promise<any>;
457475
getSandboxLogs(sandboxID: string, options?: any): Promise<any>;
458476
getSandbox(sandboxID: string): Promise<any>;
477+
getSandboxInjections(sandboxID: string): Promise<SandboxInjectionsResponse>;
478+
updateSandboxInjections(sandboxID: string, injections: SandboxInjection[]): Promise<null>;
479+
updateSandboxGithubToken(sandboxID: string, authorizationToken: string): Promise<null>;
459480
deleteSandbox(sandboxID: string): Promise<null>;
460481
killSandbox(sandboxID: string): Promise<null>;
461482
getSandboxMetrics(sandboxID: string, options?: any): Promise<any>;
@@ -472,7 +493,7 @@ export declare namespace sandbox {
472493
getTemplateFiles(templateID: string, hash: string): Promise<any>;
473494
listDefaultTemplates(): Promise<any>;
474495
listTemplates(options?: any): Promise<any>;
475-
getTemplate(templateID: string, options?: any): Promise<any>;
496+
getTemplate(templateID: string, options?: any): Promise<TemplateWithBuilds>;
476497
deleteTemplate(templateID: string): Promise<null>;
477498
updateTemplate(templateID: string, options?: any): Promise<any>;
478499
startTemplateBuildV2(templateID: string, buildID: string, options?: any): Promise<null>;
@@ -536,6 +557,9 @@ export declare namespace sandbox {
536557
betaPause(): Promise<null>;
537558
connect(options?: SandboxConnectOptions): Promise<this>;
538559
getInfo(): Promise<any>;
560+
getInjections(): Promise<SandboxInjectionsResponse>;
561+
updateInjections(injections: SandboxInjection[]): Promise<null>;
562+
updateGithubToken(authorizationToken: string): Promise<null>;
539563
getMetrics(options?: any): Promise<any>;
540564
getLogs(options?: any): Promise<any>;
541565
createSnapshot(options?: any): Promise<SnapshotInfo>;

qiniu/sandbox/client.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,25 @@ function normalizeSandboxListOptions (opts) {
100100
if (query && query.state) {
101101
opts.state = query.state;
102102
}
103+
if (query && query.template) {
104+
const normalizeTemplate = template => {
105+
if (typeof template === 'string') {
106+
return template;
107+
}
108+
return template && (template.templateID || template.template_id || template.id);
109+
};
110+
if (Array.isArray(query.template)) {
111+
const templates = query.template.map(normalizeTemplate).filter(Boolean);
112+
if (templates.length) {
113+
opts.template = templates;
114+
}
115+
} else {
116+
const template = normalizeTemplate(query.template);
117+
if (template) {
118+
opts.template = template;
119+
}
120+
}
121+
}
103122
return opts;
104123
}
105124

@@ -276,6 +295,46 @@ SandboxClient.prototype.getSandbox = function (sandboxID) {
276295
return this._request('GET', `/sandboxes/${encodePath(sandboxID)}`);
277296
};
278297

298+
SandboxClient.prototype.getSandboxInjections = function (sandboxID) {
299+
if (!sandboxID) {
300+
return Promise.reject(new SandboxError('sandboxID is required'));
301+
}
302+
return this._request('GET', `/sandboxes/${encodePath(sandboxID)}/injections`);
303+
};
304+
305+
SandboxClient.prototype.updateSandboxInjections = function (sandboxID, injections) {
306+
if (!sandboxID) {
307+
return Promise.reject(new SandboxError('sandboxID is required'));
308+
}
309+
if (!Array.isArray(injections)) {
310+
return Promise.reject(new SandboxError('injections must be an array'));
311+
}
312+
if (injections.some(injection => !injection || typeof injection !== 'object' || Array.isArray(injection))) {
313+
return Promise.reject(new SandboxError('injections must contain objects'));
314+
}
315+
return this._request('PUT', `/sandboxes/${encodePath(sandboxID)}/injections`, {
316+
body: {
317+
injections: injections.map(normalizeInjection)
318+
},
319+
empty: true
320+
});
321+
};
322+
323+
SandboxClient.prototype.updateSandboxGithubToken = function (sandboxID, authorizationToken) {
324+
if (!sandboxID) {
325+
return Promise.reject(new SandboxError('sandboxID is required'));
326+
}
327+
if (!authorizationToken) {
328+
return Promise.reject(new SandboxError('authorizationToken is required'));
329+
}
330+
return this._request('PUT', `/sandboxes/${encodePath(sandboxID)}/github-token`, {
331+
body: {
332+
authorization_token: authorizationToken
333+
},
334+
empty: true
335+
});
336+
};
337+
279338
SandboxClient.prototype.deleteSandbox = function (sandboxID) {
280339
if (!sandboxID) {
281340
return Promise.reject(new SandboxError('sandboxID is required'));

qiniu/sandbox/sandbox.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,18 @@ Sandbox.prototype.getInfo = function () {
216216
return this.client.getSandbox(this.sandboxId);
217217
};
218218

219+
Sandbox.prototype.getInjections = function () {
220+
return this.client.getSandboxInjections(this.sandboxId);
221+
};
222+
223+
Sandbox.prototype.updateInjections = function (injections) {
224+
return this.client.updateSandboxInjections(this.sandboxId, injections);
225+
};
226+
227+
Sandbox.prototype.updateGithubToken = function (authorizationToken) {
228+
return this.client.updateSandboxGithubToken(this.sandboxId, authorizationToken);
229+
};
230+
219231
Sandbox.prototype.refreshEnvdTokenIfNeeded = function () {
220232
if (this.envdAccessToken) {
221233
return Promise.resolve(this);

test/sandbox_client.test.js

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,119 @@ describe('test sandbox client module', function () {
553553
});
554554
});
555555

556+
it('maps sandbox template filters and runtime injection APIs', function () {
557+
return startServer((req, res) => {
558+
res.setHeader('Content-Type', 'application/json');
559+
if (req.method === 'GET' && req.url === '/v2/sandboxes?limit=5&template=tpl_1%2Ctpl_2%2Ctpl_3%2Ctpl_4') {
560+
res.statusCode = 200;
561+
res.end(JSON.stringify([]));
562+
return;
563+
}
564+
if (req.method === 'GET' && req.url === '/sandboxes/sbx_runtime/injections') {
565+
res.statusCode = 200;
566+
res.end(JSON.stringify({
567+
injections: [{ type: 'github', token: 'ghp_****' }]
568+
}));
569+
return;
570+
}
571+
if (req.method === 'PUT' && req.url === '/sandboxes/sbx_runtime/injections') {
572+
res.statusCode = 204;
573+
res.end();
574+
return;
575+
}
576+
if (req.method === 'PUT' && req.url === '/sandboxes/sbx_runtime/github-token') {
577+
res.statusCode = 204;
578+
res.end();
579+
return;
580+
}
581+
res.statusCode = 404;
582+
res.end(JSON.stringify({ message: req.method + ' ' + req.url }));
583+
}).then(fixture => {
584+
const client = new qiniu.sandbox.SandboxClient({
585+
endpoint: fixture.endpoint,
586+
apiKey: 'sandbox-key'
587+
});
588+
589+
return client.listSandboxesV2({
590+
limit: 5,
591+
query: {
592+
template: [
593+
'tpl_1',
594+
{ templateID: 'tpl_2' },
595+
{ template_id: 'tpl_3' },
596+
{ id: 'tpl_4' },
597+
null
598+
]
599+
}
600+
}).then(() => client.getSandboxInjections('sbx_runtime'))
601+
.then(result => {
602+
result.injections[0].token.should.eql('ghp_****');
603+
return client.updateSandboxInjections('sbx_runtime', [{
604+
type: 'openai',
605+
apiKey: 'secret',
606+
baseUrl: 'https://api.openai.com/v1/*'
607+
}]);
608+
})
609+
.then(() => client.updateSandboxGithubToken('sbx_runtime', 'github-token'))
610+
.then(() => {
611+
JSON.parse(fixture.requests[2].body).should.eql({
612+
injections: [{
613+
type: 'openai',
614+
api_key: 'secret',
615+
base_url: 'https://api.openai.com/v1/*'
616+
}]
617+
});
618+
JSON.parse(fixture.requests[3].body).should.eql({
619+
authorization_token: 'github-token'
620+
});
621+
}).then(() => closeServer(fixture.server), err => {
622+
return closeServer(fixture.server).then(() => {
623+
throw err;
624+
});
625+
});
626+
});
627+
});
628+
629+
it('validates runtime injection API arguments before sending requests', function () {
630+
return startServer((req, res) => {
631+
res.statusCode = 404;
632+
res.end();
633+
}).then(fixture => {
634+
const client = new qiniu.sandbox.SandboxClient({
635+
endpoint: fixture.endpoint,
636+
apiKey: 'sandbox-key'
637+
});
638+
const calls = [
639+
() => client.getSandboxInjections(),
640+
() => client.updateSandboxInjections(undefined, []),
641+
() => client.updateSandboxGithubToken(undefined, 'github-token'),
642+
() => client.updateSandboxInjections('sbx_runtime', { type: 'id', id: 'rule_1' }),
643+
() => client.updateSandboxGithubToken('sbx_runtime'),
644+
() => client.updateSandboxInjections('sbx_runtime', [null])
645+
];
646+
647+
return calls.reduce((promise, call, index) => {
648+
return promise.then(() => call().then(() => {
649+
throw new Error('expected runtime injection argument validation to fail');
650+
}, err => {
651+
err.name.should.eql('SandboxError');
652+
const expected = index < 3
653+
? 'sandboxID is required'
654+
: (index === 3
655+
? 'injections must be an array'
656+
: (index === 4 ? 'authorizationToken is required' : 'injections must contain objects'));
657+
err.message.should.eql(expected);
658+
}));
659+
}, Promise.resolve()).then(() => {
660+
fixture.requests.should.have.length(0);
661+
}).then(() => closeServer(fixture.server), err => {
662+
return closeServer(fixture.server).then(() => {
663+
throw err;
664+
});
665+
});
666+
});
667+
});
668+
556669
it('surfaces sandbox API string errors and default connect timeout', function () {
557670
return startServer((req, res) => {
558671
if (req.method === 'POST' && req.url === '/sandboxes/sbx_default/connect') {

0 commit comments

Comments
 (0)