Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(features): add experimental features management #785

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
38 changes: 32 additions & 6 deletions bin/clever.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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 = {
Expand All @@ -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' }),
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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({
Expand Down
47 changes: 47 additions & 0 deletions src/commands/features.js
Original file line number Diff line number Diff line change
@@ -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 };
38 changes: 37 additions & 1 deletion src/models/configuration.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
};

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -100,11 +135,12 @@ const conf = env.getOrElseAll({
SSH_GATEWAY: '[email protected]',

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',

CLEVER_CONFIGURATION_DIR: path.resolve('.', 'clevercloud'),
APP_CONFIGURATION_FILE: path.resolve('.', '.clever.json'),
});

module.exports = { conf, loadOAuthConf, writeOAuthConf, loadIdsCache, writeIdsCache };
module.exports = { conf, loadOAuthConf, writeOAuthConf, loadFeaturesConf, getFeatures, setFeature, loadIdsCache, writeIdsCache };
5 changes: 5 additions & 0 deletions src/models/features.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use strict';

const AVAILABLE_FEATURES = ['faas', 'kv', 'ng'];

module.exports = { AVAILABLE_FEATURES };
Loading