Skip to content

Commit b4abc9c

Browse files
davlgdhsablonniere
andcommitted
feat: add functions command
Co-Authored-By: Hubert SABLONNIÈRE <236342+hsablonniere@users.noreply.github.com>
1 parent 497aaef commit b4abc9c

File tree

2 files changed

+265
-1
lines changed

2 files changed

+265
-1
lines changed

bin/clever.js

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import * as domain from '../src/commands/domain.js';
3838
import * as drain from '../src/commands/drain.js';
3939
import * as env from '../src/commands/env.js';
4040
import * as features from '../src/commands/features.js';
41+
import * as functions from '../src/commands/functions.js';
4142
import * as kv from '../src/commands/kv.js';
4243
import * as link from '../src/commands/link.js';
4344
import * as login from '../src/commands/login.js';
@@ -92,7 +93,15 @@ async function run () {
9293

9394
// ARGUMENTS
9495
const args = {
95-
kvRawCommand: cliparse.argument('command', { description: 'The raw command to send to the Materia KV or Redis® add-on' }),
96+
faasId: cliparse.argument('faas-id', {
97+
description: 'Function ID',
98+
}),
99+
faasFile: cliparse.argument('filename', {
100+
description: 'Path to the function code',
101+
}),
102+
kvRawCommand: cliparse.argument('command', {
103+
description: 'The raw command to send to the Materia KV or Redis® add-on',
104+
}),
96105
kvIdOrName: cliparse.argument('kv-id', {
97106
description: 'Add-on/Real ID (or name, if unambiguous) of a Materia KV or Redis® add-on',
98107
}),
@@ -774,6 +783,29 @@ async function run () {
774783
commands: [enableFeatureCommand, disableFeatureCommand, listFeaturesCommand, infoFeaturesCommand],
775784
}, features.list);
776785

786+
// FUNCTIONS COMMANDS
787+
const functionsCreateCommand = cliparse.command('create', {
788+
description: 'Create a Clever Cloud Function',
789+
}, functions.create);
790+
const functionsDeleteCommand = cliparse.command('delete', {
791+
description: 'Delete a Clever Cloud Function',
792+
args: [args.faasId],
793+
}, functions.destroy);
794+
const functionsDeployCommand = cliparse.command('deploy', {
795+
description: 'Deploy a Clever Cloud Function from compatible source code',
796+
args: [args.faasFile, args.faasId],
797+
}, functions.deploy);
798+
const functionsListDeploymentsCommand = cliparse.command('list-deployments', {
799+
description: 'List deployments of a Clever Cloud Function',
800+
args: [args.faasId],
801+
options: [opts.humanJsonOutputFormat],
802+
}, functions.listDeployments);
803+
const functionsCommand = cliparse.command('functions', {
804+
description: 'Manage Clever Cloud Functions',
805+
options: [opts.orgaIdOrName],
806+
commands: [functionsCreateCommand, functionsDeleteCommand, functionsDeployCommand, functionsListDeploymentsCommand],
807+
}, functions.list);
808+
777809
// KV COMMAND
778810
const kvRawCommand = cliparse.command('kv', {
779811
description: 'Send a raw command to a Materia KV or Redis® add-on',
@@ -1086,6 +1118,7 @@ async function run () {
10861118
emailNotificationsCommand,
10871119
envCommands,
10881120
featuresCommands,
1121+
functionsCommand,
10891122
cliparseCommands.helpCommand,
10901123
loginCommand,
10911124
logoutCommand,

src/commands/functions.js

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
import fs from 'node:fs';
2+
import colors from 'colors/safe.js';
3+
4+
import * as User from '../models/user.js';
5+
import * as Organisation from '../models/organisation.js';
6+
7+
import { Logger } from '../logger.js';
8+
import { setTimeout } from 'timers/promises';
9+
import { sendToApi } from '../models/send-to-api.js';
10+
import { uploadFunction } from '../models/functions.js';
11+
import { createFunction, createDeployment, getDeployments, getDeployment, getFunctions, deleteDeployment, triggerDeployment, deleteFunction } from '../models/functions-api.js';
12+
13+
const DEFAULT_MAX_INSTANCES = 1;
14+
const DEFAULT_MAX_MEMORY = 64 * 1024 * 1024;
15+
16+
const PLATFORMS = {
17+
asc: 'ASSEMBLY_SCRIPT',
18+
js: 'JAVA_SCRIPT',
19+
rs: 'RUST',
20+
go: 'TINY_GO',
21+
};
22+
23+
/**
24+
* Creates a new function
25+
* @param {Object} params
26+
* @param {Object} params.options
27+
* @param {Object} params.options.org - The organisation to create the function in
28+
* @returns {Promise<void>}
29+
* */
30+
export async function create (params) {
31+
const { org } = params.options;
32+
33+
const ownerId = (org != null && org.orga_name !== '')
34+
? await Organisation.getId(org)
35+
: (await User.getCurrent()).id;
36+
37+
const createdFunction = await createFunction({ ownerId }, {
38+
name: null,
39+
description: null,
40+
environment: {},
41+
tag: null,
42+
maxInstances: DEFAULT_MAX_INSTANCES,
43+
maxMemory: DEFAULT_MAX_MEMORY,
44+
}).then(sendToApi);
45+
46+
Logger.println(`${colors.green('✓')} Function ${colors.green(createdFunction.id)} successfully created!`);
47+
}
48+
49+
/**
50+
* Deploys a function
51+
* @param {Object} params
52+
* @param {Object} params.args
53+
* @param {string} params.args[0] - The file to deploy
54+
* @param {string} params.args[1] - The function ID to deploy to
55+
* @param {Object} params.options
56+
* @param {Object} params.options.org - The organisation to deploy the function to
57+
* @returns {Promise<void>}
58+
* @throws {Error} - If the file to deploy does not exist
59+
* @throws {Error} - If the function to deploy to does not exist
60+
* */
61+
export async function deploy (params) {
62+
const [functionFile, functionId] = params.args;
63+
const { org } = params.options;
64+
65+
const ownerId = (org != null && org.orga_name !== '')
66+
? await Organisation.getId(org)
67+
: (await User.getCurrent()).id;
68+
69+
if (!fs.existsSync(functionFile)) {
70+
throw new Error(`File ${colors.red(functionFile)} does not exist, it can't be deployed`);
71+
}
72+
73+
const functions = await getFunctions({ ownerId }).then(sendToApi);
74+
const functionToDeploy = functions.find((f) => f.id === functionId);
75+
76+
if (!functionToDeploy) {
77+
throw new Error(`Function ${colors.red(functionId)} not found, it can't be deployed`);
78+
}
79+
80+
Logger.info(`Deploying ${functionFile}`);
81+
Logger.info(`Deploying to function ${functionId} of user ${ownerId}`);
82+
83+
const fileExtension = functionFile.split('.').pop();
84+
const platform = PLATFORMS[fileExtension];
85+
86+
if (!platform) {
87+
throw new Error(`File ${colors.red(functionFile)} is not a valid function file, it must be a .go, .js or .rs file`);
88+
}
89+
90+
let deployment = await createDeployment({
91+
ownerId,
92+
functionId,
93+
}, {
94+
name: null,
95+
description: null,
96+
tag: null,
97+
platform,
98+
}).then(sendToApi);
99+
100+
Logger.debug(JSON.stringify(deployment, null, 2));
101+
102+
await uploadFunction(deployment.uploadUrl, functionFile);
103+
await triggerDeployment({
104+
ownerId,
105+
functionId,
106+
deploymentId: deployment.id,
107+
}).then(sendToApi);
108+
109+
Logger.println(`${colors.green('✓')} Function compiled and uploaded successfully!`);
110+
111+
await setTimeout(1_000);
112+
while (deployment.status !== 'READY') {
113+
Logger.debug(`Deployment status: ${deployment.status}`);
114+
deployment = await getDeployment({
115+
ownerId,
116+
functionId,
117+
deploymentId: deployment.id,
118+
}).then(sendToApi);
119+
await setTimeout(1_000);
120+
}
121+
122+
Logger.println(`${colors.green('✓')} Your function is now deployed!`);
123+
Logger.println(` └─ Test it: ${colors.blue(`curl https://functions-technical-preview.services.clever-cloud.com/${functionId}`)}`);
124+
}
125+
126+
/**
127+
* Destroys a function and its deployments
128+
* @param {Object} params
129+
* @param {Object} params.args
130+
* @param {string} params.args[0] - The function ID to destroy
131+
* @param {Object} params.options
132+
* @param {Object} params.options.org - The organisation to destroy the function from
133+
* @returns {Promise<void>}
134+
* @throws {Error} - If the function to destroy does not exist
135+
* */
136+
export async function destroy (params) {
137+
const [functionId] = params.args;
138+
const { org } = params.options;
139+
140+
const ownerId = (org != null && org.orga_name !== '')
141+
? await Organisation.getId(org)
142+
: (await User.getCurrent()).id;
143+
144+
const functions = await getFunctions({ ownerId }).then(sendToApi);
145+
const functionToDelete = functions.find((f) => f.id === functionId);
146+
147+
if (!functionToDelete) {
148+
throw new Error(`Function ${colors.red(functionId)} not found, it can't be deleted`);
149+
}
150+
151+
const deployments = await getDeployments({ ownerId, functionId }).then(sendToApi);
152+
153+
deployments.forEach(async (d) => {
154+
await deleteDeployment({ ownerId, functionId, deploymentId: d.id }).then(sendToApi);
155+
});
156+
157+
await deleteFunction({ ownerId, functionId }).then(sendToApi);
158+
Logger.println(`${colors.green('✓')} Function ${colors.green(functionId)} and its deployments successfully deleted!`);
159+
}
160+
161+
/**
162+
* Lists all the functions of the current user or the current organisation
163+
* @param {Object} params
164+
* @param {Object} params.options
165+
* @param {Object} params.options.org - The organisation to list the functions from
166+
* @param {string} params.options.format - The format to display the functions
167+
* @returns {Promise<void>}
168+
*/
169+
export async function list (params) {
170+
const { org, format } = params.options;
171+
172+
const ownerId = (org != null && org.orga_name !== '')
173+
? await Organisation.getId(org)
174+
: (await User.getCurrent()).id;
175+
176+
const functions = await getFunctions({
177+
ownerId: ownerId,
178+
}).then(sendToApi);
179+
180+
if (functions.length < 1) {
181+
Logger.println(`${colors.blue('🔎')} No functions found, create one with ${colors.blue('clever functions create')} command`);
182+
return;
183+
}
184+
185+
switch (format) {
186+
case 'json':
187+
console.log(JSON.stringify(functions, null, 2));
188+
break;
189+
case 'human':
190+
default:
191+
console.table(functions, ['id', 'createdAt', 'updatedAt']);
192+
}
193+
}
194+
195+
/**
196+
* Lists all the deployments of a function
197+
* @param {Object} params
198+
* @param {Object} params.args
199+
* @param {string} params.args[0] - The function ID to list the deployments from
200+
* @param {Object} params.options
201+
* @param {Object} params.options.org - The organisation to list the deployments from
202+
* @param {string} params.options.format - The format to display the deployments
203+
* @returns {Promise<void>}
204+
* */
205+
export async function listDeployments (params) {
206+
const [functionId] = params.args;
207+
const { org, format } = params.options;
208+
209+
const ownerId = (org != null && org.orga_name !== '')
210+
? await Organisation.getId(org)
211+
: (await User.getCurrent()).id;
212+
213+
const deploymentsList = await getDeployments({
214+
ownerId: ownerId, functionId,
215+
}).then(sendToApi);
216+
217+
if (deploymentsList.length < 1) {
218+
Logger.println(`${colors.blue('🔎')} No deployments found for this function`);
219+
return;
220+
}
221+
222+
switch (format) {
223+
case 'json':
224+
console.log(JSON.stringify(deploymentsList, null, 2));
225+
break;
226+
case 'human':
227+
default:
228+
console.table(deploymentsList, ['id', 'status', 'createdAt', 'updatedAt']);
229+
console.log(`▶️ You can call your function with ${colors.blue(`curl https://functions-technical-preview.services.clever-cloud.com/${functionId}`)}`);
230+
}
231+
}

0 commit comments

Comments
 (0)