Skip to content

Commit 82896a3

Browse files
authored
Support serverless invoke (#86)
* Add serverless invoke command * Updating naming
1 parent a346745 commit 82896a3

File tree

14 files changed

+283
-50
lines changed

14 files changed

+283
-50
lines changed

docs/tests.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,15 @@ export SCW_URL=<url-to-functions-api>
3434

3535
### Run Tests
3636

37-
We provided multiple test suites, as described above, with the following npm scripts:
37+
We provided multiple test suites, as described above, with the following `npm` scripts:
38+
3839
- `npm run test`: Run all test suites
3940
- `npm run test:functions`: Run functions's test suite
4041
- `npm run test:containers`: Run containers's test suite
4142
- `npm run test:runtimes`: Run runtimes's test suite
43+
- `npm run test -- -t "Some test regex*"`: Runs all tests matching the regex
44+
45+
These tests use [Jest](https://jestjs.io/docs/) under the hood.
4246

4347
**Also, make sure that you did not install this repository inside a `node_modules` folder, otherwhise your npm commands won't work (`no tests found`)**.
4448

index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
const ScalewayProvider = require('./provider/scalewayProvider');
44
const ScalewayDeploy = require('./deploy/scalewayDeploy');
55
const ScalewayRemove = require('./remove/scalewayRemove');
6+
const ScalewayInvoke = require('./invoke/scalewayInvoke');
67
const ScalewayJwt = require('./jwt/scalewayJwt');
78
const ScalewayLogs = require('./logs/scalewayLogs');
89
const ScalewayInfo = require('./info/scalewayInfo');
@@ -15,6 +16,7 @@ class ScalewayIndex {
1516
this.serverless.pluginManager.addPlugin(ScalewayProvider);
1617
this.serverless.pluginManager.addPlugin(ScalewayDeploy);
1718
this.serverless.pluginManager.addPlugin(ScalewayRemove);
19+
this.serverless.pluginManager.addPlugin(ScalewayInvoke);
1820
this.serverless.pluginManager.addPlugin(ScalewayJwt);
1921
this.serverless.pluginManager.addPlugin(ScalewayLogs);
2022
this.serverless.pluginManager.addPlugin(ScalewayInfo);

invoke/scalewayInvoke.js

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
const BbPromise = require('bluebird');
2+
const axios = require('axios');
3+
const util = require('util');
4+
5+
const scalewayApi = require('../shared/api/endpoint');
6+
const setUpDeployment = require('../shared/setUpDeployment');
7+
const validate = require('../shared/validate');
8+
9+
class ScalewayInvoke {
10+
constructor(serverless, options) {
11+
this.serverless = serverless;
12+
this.options = options || {};
13+
this.provider = this.serverless.getProvider('scaleway');
14+
this.provider.initialize(this.serverless, this.options);
15+
16+
const api = scalewayApi.getApi(this);
17+
18+
Object.assign(
19+
this,
20+
validate,
21+
setUpDeployment,
22+
api,
23+
);
24+
25+
this.isContainer = false;
26+
this.isFunction = false;
27+
28+
function validateFunctionOrContainer() {
29+
// Check the user has specified a name, and that it's defined as either a function or container
30+
if(!this.options.function) {
31+
const msg = 'Function or container not specified';
32+
this.serverless.cli.log(msg);
33+
throw new Error(msg);
34+
}
35+
36+
this.isContainer = this.isDefinedContainer(this.options.function);
37+
this.isFunction = this.isDefinedFunction(this.options.function);
38+
39+
if(!this.isContainer && !this.isFunction) {
40+
const msg = `Function or container ${this.options.function} not defined in servleress.yml`;
41+
this.serverless.cli.log(msg);
42+
throw new Error(msg);
43+
}
44+
}
45+
46+
function lookUpFunctionOrContainer(ns) {
47+
// List containers/functions in the namespace
48+
let found = null;
49+
if(this.isContainer) {
50+
return this.listContainers(ns.id);
51+
} else {
52+
return this.listFunctions(ns.id);
53+
}
54+
}
55+
56+
function doInvoke(found) {
57+
// Filter on name
58+
let func = found.find((f) => f.name === this.options.function);
59+
const url = 'https://' + func.domain_name;
60+
61+
// Invoke
62+
axios.get(url).then(res => {
63+
// Make sure we write to stdout here to ensure we can capture output
64+
process.stdout.write(JSON.stringify(res.data));
65+
}).
66+
catch(error => {
67+
process.stderr.write(error);
68+
});
69+
}
70+
71+
this.hooks = {
72+
'before:invoke:invoke': () => BbPromise.bind(this)
73+
.then(this.setUpDeployment)
74+
.then(this.validate),
75+
'invoke:invoke': () => BbPromise.bind(this)
76+
.then(validateFunctionOrContainer)
77+
.then(() => this.getNamespaceFromList(this.namespaceName))
78+
.then(lookUpFunctionOrContainer)
79+
.then(doInvoke)
80+
};
81+
}
82+
}
83+
84+
module.exports = ScalewayInvoke;

shared/child-process.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
'use strict';
2+
3+
const child_process = require('child_process');
4+
5+
function execSync(command, options = null) {
6+
// Same as native but outputs std in case of error
7+
try {
8+
return child_process.execSync(command, options);
9+
} catch (error) {
10+
if (error.stdout) process.stdout.write(error.stdout);
11+
if (error.stderr) process.stderr.write(error.stderr);
12+
throw error;
13+
}
14+
}
15+
16+
function execCaptureOutput(command, args) {
17+
let child = child_process.spawnSync(command, args, { encoding : 'utf8' });
18+
19+
if(child.error) {
20+
if(child.stdout) process.stdout.write(child.stdout);
21+
if(child.stderr) process.stderr.write(child.stderr);
22+
throw child.error;
23+
}
24+
25+
return child.stdout;
26+
}
27+
28+
module.exports = { execSync, execCaptureOutput };

shared/validate.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,4 +262,36 @@ module.exports = {
262262

263263
return errors;
264264
},
265+
266+
isDefinedContainer(containerName) {
267+
// Check if given name is listed as a container
268+
let res = false;
269+
if(this.provider.serverless.service.custom
270+
&& this.provider.serverless.service.custom.containers) {
271+
272+
let foundKey = Object.keys(this.provider.serverless.service.custom.containers)
273+
.find((k) => k == containerName);
274+
275+
if(foundKey) {
276+
res = true;
277+
}
278+
}
279+
280+
return res;
281+
},
282+
283+
isDefinedFunction(functionName) {
284+
// Check if given name is listed as a function
285+
let res = false;
286+
if(this.provider.serverless.service.functions) {
287+
let foundKey = Object.keys(this.provider.serverless.service.functions)
288+
.find((k) => k == functionName);
289+
290+
if(foundKey) {
291+
res = true;
292+
}
293+
}
294+
295+
return res;
296+
},
265297
};

tests/containers/containers.test.js

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ const path = require('path');
44
const fs = require('fs');
55
const axios = require('axios');
66
const { expect } = require('chai');
7-
const { execSync } = require('../utils/child-process');
7+
88
const { getTmpDirPath, replaceTextInFile } = require('../utils/fs');
99
const { getServiceName, sleep } = require('../utils/misc');
1010
const { ContainerApi, RegistryApi } = require('../../shared/api');
1111
const { CONTAINERS_API_URL, REGISTRY_API_URL } = require('../../shared/constants');
12+
const { execSync, execCaptureOutput } = require('../../shared/child-process');
1213

1314
const serverlessExec = path.join('serverless');
1415

@@ -25,6 +26,7 @@ describe('Service Lifecyle Integration Test', () => {
2526
let api;
2627
let registryApi;
2728
let namespace;
29+
let containerName;
2830

2931
beforeAll(() => {
3032
oldCwd = process.cwd();
@@ -53,13 +55,15 @@ describe('Service Lifecyle Integration Test', () => {
5355
execSync(`${serverlessExec} deploy`);
5456
namespace = await api.getNamespaceFromList(serviceName);
5557
namespace.containers = await api.listContainers(namespace.id);
58+
containerName = namespace.containers[0].name;
5659
});
5760

5861
it('should invoke container from scaleway', async () => {
62+
// TODO query function status instead of having an arbitrary sleep
5963
await sleep(30000);
60-
const deployedContainer = namespace.containers[0];
61-
const response = await axios.get(`https://${deployedContainer.domain_name}`);
62-
expect(response.data.message).to.be.equal('Hello, World from Scaleway Container !');
64+
65+
let output = execCaptureOutput(serverlessExec, ['invoke', '--function', containerName]);
66+
expect(output).to.be.equal('{"message":"Hello, World from Scaleway Container !"}');
6367
});
6468

6569
it('should deploy updated service/container to scaleway', () => {
@@ -69,9 +73,9 @@ describe('Service Lifecyle Integration Test', () => {
6973

7074
it('should invoke updated container from scaleway', async () => {
7175
await sleep(30000);
72-
const deployedContainer = namespace.containers[0];
73-
const response = await axios.get(`https://${deployedContainer.domain_name}`);
74-
expect(response.data.message).to.be.equal('Container successfully updated');
76+
77+
let output = execCaptureOutput(serverlessExec, ['invoke', '--function', containerName]);
78+
expect(output).to.be.equal('{"message":"Container successfully updated"}');
7579
});
7680

7781
it('should remove service from scaleway', async () => {

tests/functions/functions.test.js

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ const fs = require('fs');
55
const axios = require('axios');
66
const { expect } = require('chai');
77
const { expect: jestExpect } = require('@jest/globals');
8-
const { execSync } = require('../utils/child-process');
8+
99
const { getTmpDirPath, replaceTextInFile } = require('../utils/fs');
1010
const { getServiceName, sleep } = require('../utils/misc');
1111
const { FunctionApi, RegistryApi } = require('../../shared/api');
1212
const { FUNCTIONS_API_URL, REGISTRY_API_URL } = require('../../shared/constants');
13+
const { execSync, execCaptureOutput } = require('../../shared/child-process');
1314
const { validateRuntime } = require('../../deploy/lib/createFunctions');
1415

1516
const serverlessExec = path.join('serverless');
@@ -27,6 +28,7 @@ describe('Service Lifecyle Integration Test', () => {
2728
let api;
2829
let registryApi;
2930
let namespace;
31+
let functionName;
3032

3133
beforeAll(() => {
3234
oldCwd = process.cwd();
@@ -55,35 +57,37 @@ describe('Service Lifecyle Integration Test', () => {
5557
execSync(`${serverlessExec} deploy`);
5658
namespace = await api.getNamespaceFromList(serviceName);
5759
namespace.functions = await api.listFunctions(namespace.id);
60+
functionName = namespace.functions[0].name;
5861
});
5962

6063
it('should invoke function from scaleway', async () => {
64+
// TODO query function status instead of having an arbitrary sleep
6165
await sleep(30000);
62-
const deployedFunction = namespace.functions[0];
63-
const response = await axios.get(`https://${deployedFunction.domain_name}`);
64-
expect(response.data.message).to.be.equal('Hello from Serverless Framework and Scaleway Functions :D');
66+
67+
let output = execCaptureOutput(serverlessExec, ['invoke', '--function', functionName]);
68+
expect(output).to.be.equal('{"message":"Hello from Serverless Framework and Scaleway Functions :D"}');
6569
});
6670

6771
it('should deploy updated service to scaleway', () => {
68-
const newHandler = `
69-
'use strict';
72+
const newJsHandler = `
73+
'use strict';
7074
71-
module.exports.handle = (event, context, cb) => {
72-
return {
73-
body: { message: 'Serverless Update Succeeded' }
74-
};
75-
}
76-
`;
75+
module.exports.handle = (event, context, cb) => {
76+
return {
77+
message: 'Serverless Update Succeeded',
78+
};
79+
};
80+
`;
7781

78-
fs.writeFileSync(path.join(tmpDir, 'handler.js'), newHandler);
82+
fs.writeFileSync(path.join(tmpDir, 'handler.js'), newJsHandler);
7983
execSync(`${serverlessExec} deploy`);
8084
});
8185

8286
it('should invoke updated function from scaleway', async () => {
8387
await sleep(30000);
84-
const deployedFunction = namespace.functions[0];
85-
const response = await axios.get(`https://${deployedFunction.domain_name}`);
86-
expect(response.data.body.message).to.be.equal('Serverless Update Succeeded');
88+
89+
let output = execCaptureOutput(serverlessExec, ['invoke', '--function', functionName]);
90+
expect(output).to.be.equal('{"message":"Serverless Update Succeeded"}');
8791
});
8892

8993
it('should deploy function with another available runtime', async () => {
@@ -100,16 +104,16 @@ def handle(event, context):
100104
return {
101105
"message": "Hello From Python310 runtime on Serverless Framework and Scaleway Functions"
102106
}
103-
`;
107+
`;
104108
fs.writeFileSync(path.join(tmpDir, 'handler.py'), pythonHandler);
105109
execSync(`${serverlessExec} deploy`);
106110
});
107111

108112
it('should invoke function with runtime updated from scaleway', async () => {
109113
await sleep(30000);
110-
const deployedFunction = namespace.functions[0];
111-
const response = await axios.get(`https://${deployedFunction.domain_name}`);
112-
expect(response.data.message).to.be.equal('Hello From Python310 runtime on Serverless Framework and Scaleway Functions');
114+
115+
let output = execCaptureOutput(serverlessExec, ['invoke', '--function', functionName]);
116+
expect(output).to.be.equal('{"message":"Hello From Python310 runtime on Serverless Framework and Scaleway Functions"}');
113117
});
114118

115119
it('should remove service from scaleway', async () => {

tests/multi-region/multi_region.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ const path = require('path');
44
const fs = require('fs');
55
const axios = require('axios');
66
const { expect } = require('chai');
7-
const { execSync } = require('../utils/child-process');
7+
const { execSync } = require('../../shared/child-process');
88
const { getTmpDirPath, replaceTextInFile } = require('../utils/fs');
99
const { getServiceName, sleep } = require('../utils/misc');
1010
const { FunctionApi, RegistryApi } = require('../../shared/api');

tests/runtimes/runtimes.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ const path = require('path');
44
const fs = require('fs');
55
const axios = require('axios');
66
const { expect } = require('chai');
7-
const { execSync } = require('../utils/child-process');
7+
const { execSync } = require('../../shared/child-process');
88
const { getTmpDirPath } = require('../utils/fs');
99
const { getServiceName, createTestService, sleep } = require('../utils/misc');
1010
const { FunctionApi, RegistryApi, ContainerApi } = require('../../shared/api');
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
const { expect } = require('chai');
2+
const { expect: jestExpect } = require('@jest/globals');
3+
4+
const { execSync, execCaptureOutput } = require('../../shared/child-process');
5+
6+
describe('Synchronous command execution test', () => {
7+
it('should execute a command synchronously', () => {
8+
execSync('ls');
9+
});
10+
11+
it('should throw an error for an invalid command', () => {
12+
expect(() => {
13+
execSync('blah');
14+
}).to.throw();
15+
});
16+
});
17+
18+
describe('Synchronous output capture of command test', () => {
19+
it('should capture the output of a command', () => {
20+
let output = execCaptureOutput('echo', ['foo bar']);
21+
expect(output).to.equal('foo bar\n');
22+
});
23+
});

0 commit comments

Comments
 (0)