Skip to content

Commit

Permalink
feat: add functions command
Browse files Browse the repository at this point in the history
Co-Authored-By: Hubert SABLONNIÈRE <236342+hsablonniere@users.noreply.github.com>
  • Loading branch information
davlgd and hsablonniere committed Feb 4, 2025
1 parent cd870b5 commit 3606b96
Show file tree
Hide file tree
Showing 2 changed files with 245 additions and 0 deletions.
30 changes: 30 additions & 0 deletions 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 @@ -91,6 +92,12 @@ async function run () {

// ARGUMENTS
const args = {
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',
}),
Expand Down Expand Up @@ -767,6 +774,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
215 changes: 215 additions & 0 deletions src/commands/functions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
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;

/**
* 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}`);

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

await uploadFunction(deployment.uploadUrl, functionFile);

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

Logger.println(`${colors.green('✓')} Function compiled and uploaded successfully!`);

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

Logger.println(`${colors.green('✓')} Your function is now 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}`)}`);
}
}

0 comments on commit 3606b96

Please sign in to comment.