From 3606b96388b59000f6a2ea06a8d2b08c0c32014d Mon Sep 17 00:00:00 2001 From: David Legrand <1110600+davlgd@users.noreply.github.com> Date: Sun, 2 Feb 2025 15:48:49 +0100 Subject: [PATCH] feat: add functions command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Hubert SABLONNIÈRE <236342+hsablonniere@users.noreply.github.com> --- bin/clever.js | 30 ++++++ src/commands/functions.js | 215 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 245 insertions(+) create mode 100644 src/commands/functions.js diff --git a/bin/clever.js b/bin/clever.js index e7f0e82..177484d 100755 --- a/bin/clever.js +++ b/bin/clever.js @@ -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'; @@ -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', }), @@ -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', diff --git a/src/commands/functions.js b/src/commands/functions.js new file mode 100644 index 0000000..0d2f151 --- /dev/null +++ b/src/commands/functions.js @@ -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} + * */ +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} + * @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} + * @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} + */ +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} + * */ +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}`)}`); + } +}