From a754de42c025924f616ebc516e0ee9a850877840 Mon Sep 17 00:00:00 2001 From: David Legrand Date: Fri, 28 Jun 2024 00:19:10 +0200 Subject: [PATCH] feat(features): add experimental features management --- bin/clever.js | 38 +++++++++++++++++++++++++----- src/commands/features.js | 47 +++++++++++++++++++++++++++++++++++++ src/models/configuration.js | 38 +++++++++++++++++++++++++++++- src/models/features.js | 5 ++++ 4 files changed, 121 insertions(+), 7 deletions(-) create mode 100644 src/commands/features.js create mode 100644 src/models/features.js diff --git a/bin/clever.js b/bin/clever.js index b55d541..12b777a 100755 --- a/bin/clever.js +++ b/bin/clever.js @@ -58,6 +58,7 @@ const handleCommandPromise = require('../src/command-promise-handler.js'); const Formatter = require('../src/models/format-string.js'); const { AVAILABLE_ZONES } = require('../src/models/application.js'); const { getOutputFormatOption, getSameCommitPolicyOption, getExitOnOption } = require('../src/command-options.js'); +const { loadFeaturesConf } = require('../src/models/configuration.js'); // Exit cleanly if the program we pipe to exits abruptly process.stdout.on('error', (error) => { @@ -96,7 +97,7 @@ const Notification = lazyRequire('../src/models/notification.js'); const NetworkGroup = lazyRequire('../src/models/networkgroup.js'); const Namespaces = lazyRequire('../src/models/namespaces.js'); -function run () { +async function run () { // ARGUMENTS const args = { @@ -123,6 +124,7 @@ function run () { complete: Drain('listDrainTypes'), }), drainUrl: cliparse.argument('drain-url', { description: 'Drain URL' }), + featureName: cliparse.argument('feature-name', { description: 'Experimental feature name' }), fqdn: cliparse.argument('fqdn', { description: 'Domain name of the application' }), notificationName: cliparse.argument('name', { description: 'Notification name' }), notificationId: cliparse.argument('notification-id', { description: 'Notification ID' }), @@ -1124,10 +1126,29 @@ function run () { console.info('clever database backups download'); }); + // FEATURES COMMANDS + const features = lazyRequirePromiseModule('../src/commands/features.js'); + const listFeaturesCommand = cliparse.command('list', { + description: 'List available experimental features', + options: [opts.humanJsonOutputFormat], + }, features('list')); + const enableFeatureCommand = cliparse.command('enable', { + description: 'Enable an experimental feature', + args: [args.featureName], + }, features('enable')); + const disableFeatureCommand = cliparse.command('disable', { + description: 'Disable an experimental feature', + args: [args.featureName], + }, features('disable')); + const featuresCommands = cliparse.command('features', { + description: 'Manage experimental features', + commands: [listFeaturesCommand, enableFeatureCommand, disableFeatureCommand], + }); + // Patch help command description cliparseCommands.helpCommand.description = 'Display help about the Clever Cloud CLI'; - const commands = _sortBy([ + let commands = [ accesslogsCommand, activityCommand, addonCommands, @@ -1146,14 +1167,12 @@ function run () { drainCommands, emailNotificationsCommand, envCommands, + featuresCommands, cliparseCommands.helpCommand, loginCommand, logoutCommand, logsCommand, makeDefaultCommand, - // Not ready for stable release yet - // networkGroupsCommand, - // ngCommand, openCommand, consoleCommand, profileCommand, @@ -1167,7 +1186,14 @@ function run () { tcpRedirsCommands, versionCommand, webhooksCommand, - ], 'name'); + ]; + + // Add experimental features only if they are enabled through the configuration file + const featuresFromConf = await loadFeaturesConf(); + featuresFromConf.ng === true && commands.push(networkGroupsCommand); + + // We sort the commands by name + commands = _sortBy(commands, 'name'); // CLI PARSER const cliParser = cliparse.cli({ diff --git a/src/commands/features.js b/src/commands/features.js new file mode 100644 index 0000000..2dda3bf --- /dev/null +++ b/src/commands/features.js @@ -0,0 +1,47 @@ +'use strict'; + +const { getFeatures, setFeature } = require('../models/configuration.js'); +const { AVAILABLE_FEATURES } = require('../models/features.js'); +const Logger = require('../logger.js'); + +async function list (params) { + const { format } = params.options; + const features = await getFeatures(); + + switch (format) { + case 'json': { + Logger.printJson(features); + break; + } + case 'human': + default: { + for (const feature in features) { + console.log(`- ${feature}: ${features[feature]}`); + } + } + } +} + +async function enable (params) { + const { 'feature-name': featureName } = params.namedArgs; + + if (!AVAILABLE_FEATURES.includes(featureName)) { + throw new Error(`Feature '${featureName}' is not available`); + } + + await setFeature(featureName, true); + Logger.println(`Experimental feature '${featureName}' enabled`); +} + +async function disable (params) { + const { 'feature-name': featureName } = params.namedArgs; + + if (!AVAILABLE_FEATURES.includes(featureName)) { + throw new Error(`Feature '${featureName}' is not available`); + } + + await setFeature(featureName, false); + Logger.println(`Experimental feature '${featureName}' disabled`); +} + +module.exports = { disable, enable, list }; diff --git a/src/models/configuration.js b/src/models/configuration.js index e6189d1..f6b5aad 100644 --- a/src/models/configuration.js +++ b/src/models/configuration.js @@ -12,6 +12,7 @@ const env = commonEnv(Logger); const CONFIG_FILES = { MAIN: 'clever-tools.json', + FEATURES: 'clever-tools-features.json', IDS_CACHE: 'ids-cache.json', }; @@ -61,6 +62,40 @@ async function writeOAuthConf (oauthData) { } } +async function loadFeaturesConf () { + Logger.debug('Load features configuration from ' + conf.FEATURES_FILE); + try { + const rawFile = await fs.readFile(conf.FEATURES_FILE); + return JSON.parse(rawFile); + } + catch (error) { + Logger.info(`Cannot load experimental features configuration from ${conf.FEATURES_FILE}`); + return {}; + } +} + +async function getFeatures () { + Logger.debug('Get features configuration from ' + conf.FEATURES_FILE); + try { + const rawFile = await fs.readFile(conf.FEATURES_FILE); + return JSON.parse(rawFile); + } + catch (error) { + throw new Error(`Cannot get experimental features configuration from ${conf.FEATURES_FILE}`); + } +} + +async function setFeature (feature, value) { + const currentFeatures = await getFeatures(); + const newFeatures = { ...currentFeatures, ...{ [feature]: value } }; + try { + await fs.writeFile(conf.FEATURES_FILE, JSON.stringify(newFeatures, null, 2)); + } + catch (error) { + throw new Error(`Cannot write experimental features configuration to ${conf.FEATURES_FILE}`); + } +} + async function loadIdsCache () { const cachePath = getConfigPath(CONFIG_FILES.IDS_CACHE); try { @@ -100,6 +135,7 @@ const conf = env.getOrElseAll({ SSH_GATEWAY: 'ssh@sshgateway-clevercloud-customers.services.clever-cloud.com', CONFIGURATION_FILE: getConfigPath(CONFIG_FILES.MAIN), + FEATURES_FILE: getConfigPath(CONFIG_FILES.FEATURES), CONSOLE_TOKEN_URL: 'https://console.clever-cloud.com/cli-oauth', // CONSOLE_TOKEN_URL: 'https://next-console.cleverapps.io/cli-oauth', @@ -107,4 +143,4 @@ const conf = env.getOrElseAll({ APP_CONFIGURATION_FILE: path.resolve('.', '.clever.json'), }); -module.exports = { conf, loadOAuthConf, writeOAuthConf, loadIdsCache, writeIdsCache }; +module.exports = { conf, loadOAuthConf, writeOAuthConf, loadFeaturesConf, getFeatures, setFeature, loadIdsCache, writeIdsCache }; diff --git a/src/models/features.js b/src/models/features.js new file mode 100644 index 0000000..94af502 --- /dev/null +++ b/src/models/features.js @@ -0,0 +1,5 @@ +'use strict'; + +const AVAILABLE_FEATURES = ['faas', 'kv', 'ng']; + +module.exports = { AVAILABLE_FEATURES };