Skip to content

Commit c8a7f09

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

File tree

2 files changed

+249
-1
lines changed

2 files changed

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

0 commit comments

Comments
 (0)