Skip to content

Commit

Permalink
feat: add features command
Browse files Browse the repository at this point in the history
  • Loading branch information
davlgd committed Oct 22, 2024
1 parent f320571 commit f80d87a
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 3 deletions.
46 changes: 43 additions & 3 deletions bin/clever.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ import '../src/initial-setup.js';

import cliparse from 'cliparse';
import cliparseCommands from 'cliparse/src/command.js';
import colors from 'colors/safe.js';

Check failure on line 8 in bin/clever.js

View workflow job for this annotation

GitHub Actions / Build

'colors' is defined but never used
import _sortBy from 'lodash/sortBy.js';

import { getPackageJson } from '../src/load-package-json.cjs';
import * as git from '../src/models/git.js';
import * as Parsers from '../src/parsers.js';
import { handleCommandPromise } from '../src/command-promise-handler.js';
import { AVAILABLE_ZONES } from '../src/models/application.js';
import { EXPERIMENTAL_FEATURES } from '../src/experimental-features.js';

Check failure on line 16 in bin/clever.js

View workflow job for this annotation

GitHub Actions / Build

'EXPERIMENTAL_FEATURES' is defined but never used
import { loadFeaturesConf } from '../src/models/configuration.js';
import { getOutputFormatOption, getSameCommitPolicyOption, getExitOnOption } from '../src/command-options.js';

import * as Addon from '../src/models/addon.js';
Expand All @@ -34,6 +37,7 @@ import * as diag from '../src/commands/diag.js';
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 link from '../src/commands/link.js';
import * as login from '../src/commands/login.js';
import * as logout from '../src/commands/logout.js';
Expand Down Expand Up @@ -74,7 +78,7 @@ cliparse.command = function (name, options, commandFunction) {
});
};

function run () {
async function run () {

// ARGUMENTS
const args = {
Expand Down Expand Up @@ -102,6 +106,10 @@ function run () {
}),
drainUrl: cliparse.argument('drain-url', { description: 'Drain URL' }),
fqdn: cliparse.argument('fqdn', { description: 'Domain name of the application' }),
features: cliparse.argument('features', {
description: 'Comma-separated list of experimental features to manage',
parser: Parsers.commaSeparated,
}),
notificationName: cliparse.argument('name', { description: 'Notification name' }),
notificationId: cliparse.argument('notification-id', { description: 'Notification ID' }),
webhookUrl: cliparse.argument('url', { description: 'Webhook URL' }),
Expand Down Expand Up @@ -658,6 +666,24 @@ function run () {
commands: [envSetCommand, envRemoveCommand, envImportCommand, envImportVarsFromLocalEnvCommand],
}, env.list);

// EXPERIMENTAL FEATURES COMMAND
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.features],
}, features.enable);
const disableFeatureCommand = cliparse.command('disable', {
description: 'Disable an experimental feature',
args: [args.features],
}, features.disable);
const featuresCommands = cliparse.command('features', {
description: 'Manage Clever Tools experimental features',
commands: [listFeaturesCommand, enableFeatureCommand, disableFeatureCommand],
}, features.list);

// LINK COMMAND
const appLinkCommand = cliparse.command('link', {
description: 'Link this repo to an existing application',
Expand Down Expand Up @@ -873,7 +899,7 @@ function run () {
// Patch help command description
cliparseCommands.helpCommand.description = 'Display help about the Clever Cloud CLI';

const commands = _sortBy([
let commands = [
accesslogsCommand,
activityCommand,
addonCommands,
Expand All @@ -892,6 +918,7 @@ function run () {
drainCommands,
emailNotificationsCommand,
envCommands,
featuresCommands,
cliparseCommands.helpCommand,
loginCommand,
logoutCommand,
Expand All @@ -910,7 +937,20 @@ function run () {
tcpRedirsCommands,
versionCommand,
webhooksCommand,
], 'name');
];

// Add experimental features only if they are enabled through the configuration file
const featuresFromConf = await loadFeaturesConf();

Check failure on line 943 in bin/clever.js

View workflow job for this annotation

GitHub Actions / Build

'featuresFromConf' is assigned a value but never used
// Here we add the commands for the enabled features
// if (featuresFromConf.kv) {
// commands.push(kvCommand);
// }
// if (featuresFromConf.ng) {
// commands.push(ngCommand);
// }

// We sort the commands by name
commands = _sortBy(commands, 'name');

// CLI PARSER
const cliParser = cliparse.cli({
Expand Down
77 changes: 77 additions & 0 deletions src/commands/features.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { getFeatures, setFeature } from '../models/configuration.js';
import { EXPERIMENTAL_FEATURES } from '../experimental-features.js';
import { formatTable } from '../format-table.js';
import { Logger } from '../logger.js';

function getPropertyMaxWidth (array, propertyName) {
return Math.max(...array.map((o) => o[propertyName].length));
}

export async function list (params) {
const { format } = params.options;

const features_conf = await getFeatures();
const features = EXPERIMENTAL_FEATURES.map((feature) => {
const enabled = features_conf[feature.id] === true;
return { ...feature, enabled };
});

// For each feature, print the object with the id, status, description and enabled
switch (format) {
case 'json': {
Logger.printJson(features);
break;
}
case 'human':
default: {
const headers = ['ID', 'STATUS', 'DESCRIPTION', 'ENABLED'];
const columnWidths = [
getPropertyMaxWidth(features, 'id'),
getPropertyMaxWidth(features, 'status'),
getPropertyMaxWidth(features, 'description'),
getPropertyMaxWidth(features, 'enabled'),
];

// We calculate the maximum width of each column to format the table
const formatTableWithColumnWidth = formatTable(columnWidths);

Logger.println(formatTableWithColumnWidth([
headers,
...features.map((feature) => [
feature.id,
feature.status,
feature.description,
feature.enabled,
]),
]));
}
}
}

export async function enable (params) {
const { features } = params.namedArgs;
const availableFeatures = EXPERIMENTAL_FEATURES.map((feature) => feature.id);

for (const featureName of features) {
if (!availableFeatures.includes(featureName)) {
Logger.printErrorLine(`- Feature '${featureName}' is not available`);
continue;
}
await setFeature(featureName, true);
Logger.println(`- Experimental feature '${featureName}' enabled`);
}
}

export async function disable (params) {
const { features } = params.namedArgs;
const availableFeatures = EXPERIMENTAL_FEATURES.map((feature) => feature.id);

for (const featureName of features) {
if (!availableFeatures.includes(featureName)) {
Logger.printErrorLine(`- Feature '${featureName}' is not available`);
continue;
}
await setFeature(featureName, false);
Logger.println(`- Experimental feature '${featureName}' disabled`);
}
}
12 changes: 12 additions & 0 deletions src/experimental-features.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export const EXPERIMENTAL_FEATURES = [
{
id: 'kv',
status: 'alpha',
description: 'Send commands to Materia KV directly from Clever Tools, without other dependencies',
},
{
id: 'ng',
status: 'beta',
description: 'Manage Network Groups to link applications, add-ons, external peers in a Wireguard® network',
},
];
44 changes: 44 additions & 0 deletions src/models/configuration.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const env = commonEnv(Logger);
const CONFIG_FILES = {
MAIN: 'clever-tools.json',
IDS_CACHE: 'ids-cache.json',
EXPERIMENTAL_FEATURES_FILE: 'clever-tools-experimental-features.json',
};

function getConfigPath (configFile) {
Expand Down Expand Up @@ -85,6 +86,48 @@ export async function writeIdsCache (ids) {
}
}

export async function loadFeaturesConf () {
Logger.debug('Load features configuration from ' + conf.EXPERIMENTAL_FEATURES_FILE);

try {
const rawFile = await fs.readFile(conf.EXPERIMENTAL_FEATURES_FILE);
return JSON.parse(rawFile);
}
catch (error) {
if (error.code !== 'ENOENT') {
Logger.info(`Cannot load experimental features configuration from ${conf.EXPERIMENTAL_FEATURES_FILE}`);
}

return {};
}
}

export async function getFeatures () {
Logger.debug('Get features configuration from ' + conf.EXPERIMENTAL_FEATURES_FILE);
try {
const rawFile = await fs.readFile(conf.EXPERIMENTAL_FEATURES_FILE);
return JSON.parse(rawFile);
}
catch (error) {
if (error.code !== 'ENOENT') {
throw new Error(`Cannot get experimental features configuration from ${conf.EXPERIMENTAL_FEATURES_FILE}`);
}
return {};
}
}

export async function setFeature (feature, value) {
const currentFeatures = await getFeatures();
const newFeatures = { ...currentFeatures, ...{ [feature]: value } };

try {
await fs.writeFile(conf.EXPERIMENTAL_FEATURES_FILE, JSON.stringify(newFeatures, null, 2));
}
catch (error) {
throw new Error(`Cannot write experimental features configuration to ${conf.EXPERIMENTAL_FEATURES_FILE}`);
}
}

export const conf = env.getOrElseAll({
API_HOST: 'https://api.clever-cloud.com',
// API_HOST: 'https://ccapi-preprod.cleverapps.io',
Expand All @@ -98,6 +141,7 @@ export const conf = env.getOrElseAll({
SSH_GATEWAY: '[email protected]',

CONFIGURATION_FILE: getConfigPath(CONFIG_FILES.MAIN),
EXPERIMENTAL_FEATURES_FILE: getConfigPath(CONFIG_FILES.EXPERIMENTAL_FEATURES_FILE),
CONSOLE_TOKEN_URL: 'https://console.clever-cloud.com/cli-oauth',
// CONSOLE_TOKEN_URL: 'https://next-console.cleverapps.io/cli-oauth',

Expand Down

0 comments on commit f80d87a

Please sign in to comment.