Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion bin/clever.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import * as domain from '../src/commands/domain.js';
import * as drain from '../src/commands/drain.js';
import * as env from '../src/commands/env.js';
import * as features from '../src/commands/features.js';
import * as functions from '../src/commands/functions.js';
import * as kv from '../src/commands/kv.js';
import * as link from '../src/commands/link.js';
import * as login from '../src/commands/login.js';
Expand Down Expand Up @@ -92,7 +93,15 @@ async function run () {

// ARGUMENTS
const args = {
kvRawCommand: cliparse.argument('command', { description: 'The raw command to send to the Materia KV or Redis® add-on' }),
faasId: cliparse.argument('faas-id', {
description: 'Function ID',
}),
faasFile: cliparse.argument('filename', {
description: 'Path to the function code',
}),
kvRawCommand: cliparse.argument('command', {
description: 'The raw command to send to the Materia KV or Redis® add-on',
}),
kvIdOrName: cliparse.argument('kv-id', {
description: 'Add-on/Real ID (or name, if unambiguous) of a Materia KV or Redis® add-on',
}),
Expand Down Expand Up @@ -774,6 +783,29 @@ async function run () {
commands: [enableFeatureCommand, disableFeatureCommand, listFeaturesCommand, infoFeaturesCommand],
}, features.list);

// FUNCTIONS COMMANDS
const functionsCreateCommand = cliparse.command('create', {
description: 'Create a Clever Cloud Function',
}, functions.create);
const functionsDeleteCommand = cliparse.command('delete', {
description: 'Delete a Clever Cloud Function',
args: [args.faasId],
}, functions.destroy);
const functionsDeployCommand = cliparse.command('deploy', {
description: 'Deploy a Clever Cloud Function from compatible source code',
args: [args.faasFile, args.faasId],
}, functions.deploy);
const functionsListDeploymentsCommand = cliparse.command('list-deployments', {
description: 'List deployments of a Clever Cloud Function',
args: [args.faasId],
options: [opts.humanJsonOutputFormat],
}, functions.listDeployments);
const functionsCommand = cliparse.command('functions', {
description: 'Manage Clever Cloud Functions',
options: [opts.orgaIdOrName],
commands: [functionsCreateCommand, functionsDeleteCommand, functionsDeployCommand, functionsListDeploymentsCommand],
}, functions.list);

// KV COMMAND
const kvRawCommand = cliparse.command('kv', {
description: 'Send a raw command to a Materia KV or Redis® add-on',
Expand Down Expand Up @@ -1086,6 +1118,7 @@ async function run () {
emailNotificationsCommand,
envCommands,
featuresCommands,
functionsCommand,
cliparseCommands.helpCommand,
loginCommand,
logoutCommand,
Expand Down
231 changes: 231 additions & 0 deletions src/commands/functions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
import fs from 'node:fs';
import colors from 'colors/safe.js';

import * as User from '../models/user.js';
import * as Organisation from '../models/organisation.js';

import { Logger } from '../logger.js';
import { setTimeout } from 'timers/promises';
import { sendToApi } from '../models/send-to-api.js';
import { uploadFunction } from '../models/functions.js';
import { createFunction, createDeployment, getDeployments, getDeployment, getFunctions, deleteDeployment, triggerDeployment, deleteFunction } from '../models/functions-api.js';

const DEFAULT_MAX_INSTANCES = 1;
const DEFAULT_MAX_MEMORY = 64 * 1024 * 1024;

const PLATFORMS = {
ts: 'ASSEMBLY_SCRIPT',
js: 'JAVA_SCRIPT',
rs: 'RUST',
go: 'TINY_GO',
};

/**
* Creates a new function
* @param {Object} params
* @param {Object} params.options
* @param {Object} params.options.org - The organisation to create the function in
* @returns {Promise<void>}
* */
export async function create (params) {
const { org } = params.options;

const ownerId = (org != null && org.orga_name !== '')
? await Organisation.getId(org)
: (await User.getCurrent()).id;

const createdFunction = await createFunction({ ownerId }, {
name: null,
description: null,
environment: {},
tag: null,
maxInstances: DEFAULT_MAX_INSTANCES,
maxMemory: DEFAULT_MAX_MEMORY,
}).then(sendToApi);

Logger.println(`${colors.green('✓')} Function ${colors.green(createdFunction.id)} successfully created!`);
}

/**
* Deploys a function
* @param {Object} params
* @param {Object} params.args
* @param {string} params.args[0] - The file to deploy
* @param {string} params.args[1] - The function ID to deploy to
* @param {Object} params.options
* @param {Object} params.options.org - The organisation to deploy the function to
* @returns {Promise<void>}
* @throws {Error} - If the file to deploy does not exist
* @throws {Error} - If the function to deploy to does not exist
* */
export async function deploy (params) {
const [functionFile, functionId] = params.args;
const { org } = params.options;

const ownerId = (org != null && org.orga_name !== '')
? await Organisation.getId(org)
: (await User.getCurrent()).id;

if (!fs.existsSync(functionFile)) {
throw new Error(`File ${colors.red(functionFile)} does not exist, it can't be deployed`);
}

const functions = await getFunctions({ ownerId }).then(sendToApi);
const functionToDeploy = functions.find((f) => f.id === functionId);

if (!functionToDeploy) {
throw new Error(`Function ${colors.red(functionId)} not found, it can't be deployed`);
}

Logger.info(`Deploying ${functionFile}`);
Logger.info(`Deploying to function ${functionId} of user ${ownerId}`);

const fileExtension = functionFile.split('.').pop();
const platform = PLATFORMS[fileExtension];

if (!platform) {
throw new Error(`File ${colors.red(functionFile)} is not a valid function file, it must be a .${Object.keys(PLATFORMS).join(', ')} file`);
}

let deployment = await createDeployment({
ownerId,
functionId,
}, {
name: null,
description: null,
tag: null,
platform,
}).then(sendToApi);

Logger.debug(JSON.stringify(deployment, null, 2));

await uploadFunction(deployment.uploadUrl, functionFile);
await triggerDeployment({
ownerId,
functionId,
deploymentId: deployment.id,
}).then(sendToApi);

Logger.println(`${colors.green('✓')} Function file uploaded successfully, packaging from ${platform} to WASM...`);

await setTimeout(1_000);
while (deployment.status !== 'READY') {
Logger.debug(`Deployment status: ${deployment.status}`);
deployment = await getDeployment({
ownerId,
functionId,
deploymentId: deployment.id,
}).then(sendToApi);
await setTimeout(1_000);
}

Logger.println(`${colors.green('✓')} Your function is now packaged and deployed!`);
Logger.println(` └─ Test it: ${colors.blue(`curl https://functions-technical-preview.services.clever-cloud.com/${functionId}`)}`);
}

/**
* Destroys a function and its deployments
* @param {Object} params
* @param {Object} params.args
* @param {string} params.args[0] - The function ID to destroy
* @param {Object} params.options
* @param {Object} params.options.org - The organisation to destroy the function from
* @returns {Promise<void>}
* @throws {Error} - If the function to destroy does not exist
* */
export async function destroy (params) {
const [functionId] = params.args;
const { org } = params.options;

const ownerId = (org != null && org.orga_name !== '')
? await Organisation.getId(org)
: (await User.getCurrent()).id;

const functions = await getFunctions({ ownerId }).then(sendToApi);
const functionToDelete = functions.find((f) => f.id === functionId);

if (!functionToDelete) {
throw new Error(`Function ${colors.red(functionId)} not found, it can't be deleted`);
}

const deployments = await getDeployments({ ownerId, functionId }).then(sendToApi);

deployments.forEach(async (d) => {
await deleteDeployment({ ownerId, functionId, deploymentId: d.id }).then(sendToApi);
});

await deleteFunction({ ownerId, functionId }).then(sendToApi);
Logger.println(`${colors.green('✓')} Function ${colors.green(functionId)} and its deployments successfully deleted!`);
}

/**
* Lists all the functions of the current user or the current organisation
* @param {Object} params
* @param {Object} params.options
* @param {Object} params.options.org - The organisation to list the functions from
* @param {string} params.options.format - The format to display the functions
* @returns {Promise<void>}
*/
export async function list (params) {
const { org, format } = params.options;

const ownerId = (org != null && org.orga_name !== '')
? await Organisation.getId(org)
: (await User.getCurrent()).id;

const functions = await getFunctions({
ownerId: ownerId,
}).then(sendToApi);

if (functions.length < 1) {
Logger.println(`${colors.blue('🔎')} No functions found, create one with ${colors.blue('clever functions create')} command`);
return;
}

switch (format) {
case 'json':
console.log(JSON.stringify(functions, null, 2));
break;
case 'human':
default:
console.table(functions, ['id', 'createdAt', 'updatedAt']);
}
}

/**
* Lists all the deployments of a function
* @param {Object} params
* @param {Object} params.args
* @param {string} params.args[0] - The function ID to list the deployments from
* @param {Object} params.options
* @param {Object} params.options.org - The organisation to list the deployments from
* @param {string} params.options.format - The format to display the deployments
* @returns {Promise<void>}
* */
export async function listDeployments (params) {
const [functionId] = params.args;
const { org, format } = params.options;

const ownerId = (org != null && org.orga_name !== '')
? await Organisation.getId(org)
: (await User.getCurrent()).id;

const deploymentsList = await getDeployments({
ownerId: ownerId, functionId,
}).then(sendToApi);

if (deploymentsList.length < 1) {
Logger.println(`${colors.blue('🔎')} No deployments found for this function`);
return;
}

switch (format) {
case 'json':
console.log(JSON.stringify(deploymentsList, null, 2));
break;
case 'human':
default:
console.table(deploymentsList, ['id', 'status', 'createdAt', 'updatedAt']);
console.log(`▶️ You can call your function with ${colors.blue(`curl https://functions-technical-preview.services.clever-cloud.com/${functionId}`)}`);
}
}
17 changes: 17 additions & 0 deletions src/experimental-features.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,23 @@ import dedent from 'dedent';
import { conf } from './models/configuration.js';

export const EXPERIMENTAL_FEATURES = {
functions: {
status: 'alpha',
description: 'Deploy and manage serverless Functions on Clever Cloud',
instructions: `
Manage a function:
clever functions create
clever functions deploy function_xxx
clever functions delete function_xxx

List functions and deployments:

clever functions
clever functions list-deployments function_xxx

Learn more about functions: https://github.com/CleverCloud/clever-tools/blob/davlgd-new-functions/docs/functions.md
`,
},
kv: {
status: 'alpha',
description: 'Send commands to databases such as Materia KV or Redis® directly from Clever Tools, without other dependencies',
Expand Down
Loading