Skip to content

Commit

Permalink
feat: otoroshi command
Browse files Browse the repository at this point in the history
  • Loading branch information
davlgd committed Feb 1, 2025
1 parent 6d4d633 commit a101b70
Show file tree
Hide file tree
Showing 5 changed files with 672 additions and 0 deletions.
56 changes: 56 additions & 0 deletions bin/clever.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import * as ng from '../src/commands/ng.js';
import * as notifyEmail from '../src/commands/notify-email.js';
import * as open from '../src/commands/open.js';
import * as consoleModule from '../src/commands/console.js';
import * as otoroshi from '../src/commands/otoroshi.js';
import * as profile from '../src/commands/profile.js';
import * as publishedConfig from '../src/commands/published-config.js';
import * as restart from '../src/commands/restart.js';
Expand Down Expand Up @@ -93,6 +94,12 @@ async function run () {

// ARGUMENTS
const args = {
otoroshiDestination: cliparse.argument('destination', {
description: 'A destination path for a protected route (e.g. /api)',
}),
otoroshiRoute: cliparse.argument('route', {
description: 'A route to manage with Otoroshi add-on',
}),
kvRawCommand: cliparse.argument('command', {
description: 'The raw command to send to the Materia KV or Redis® add-on',
}),
Expand Down Expand Up @@ -981,9 +988,57 @@ async function run () {
options: [opts.alias, opts.appIdOrName],
}, open.open);

// OTOROSHI COMMAND
const otoroshiGetCommand = cliparse.command('get', {
description: 'Get information about the Otoroshi operator',
args: [args.addonIdOrName],
options: [opts.humanJsonOutputFormat],
}, otoroshi.get);
const otoroshiGetRoutesCommand = cliparse.command('get-routes', {
description: 'Get routes from the Otoroshi operator',
args: [args.addonIdOrName],
options: [opts.humanJsonOutputFormat],
}, otoroshi.getRoutes);
const otoroshiLinkCommand = cliparse.command('link', {
description: 'Link an application to the Otoroshi operator through a Network Group',
args: [args.appIdOrName],
}, otoroshi.link);
const otoroshiLogsCommand = cliparse.command('logs', {
description: 'Open the Otoroshi application logs in Clever Cloud Console',
args: [args.addonIdOrName],
}, otoroshi.openLogs);
const otoroshiOpenCommand = cliparse.command('open', {
description: 'Open the Otoroshi admin console in your browser',
args: [args.addonIdOrName],
commands: [otoroshiLogsCommand],
}, otoroshi.open);
const otoroshiRebootCommand = cliparse.command('reboot', {
description: 'Reboot your Otoroshi operator',
args: [args.addonIdOrName],
}, otoroshi.reboot);
const otoroshiRebuildCommand = cliparse.command('rebuild', {
description: 'Rebuild your Otoroshi operator',
args: [args.addonIdOrName],
}, otoroshi.rebuild);
const otoroshiVersionsCheckCommand = cliparse.command('check', {
description: 'Check the Otoroshi operator\'s deployed version',
args: [args.addonIdOrName],
options: [opts.humanJsonOutputFormat],
}, otoroshi.checkVersion);
const otoroshiUpdateCommand = cliparse.command('update', {
description: 'Update the Otoroshi operator\'s version and rebuild it',
args: [args.addonIdOrName],
}, otoroshi.updateVersion);
const otoroshiVersionsCommands = cliparse.command('version', {
description: 'Manage the deployed version of an Otoroshi operator',
commands: [otoroshiVersionsCheckCommand, otoroshiUpdateCommand],
}, otoroshi.checkVersion);
const otoroshiCommand = cliparse.command('otoroshi', {
description: 'Manage Clever Cloud Otoroshi services',
privateOptions: [opts.humanJsonOutputFormat],
commands: [otoroshiGetCommand, otoroshiGetRoutesCommand, otoroshiLinkCommand, otoroshiOpenCommand, otoroshiRebootCommand, otoroshiRebuildCommand, otoroshiVersionsCommands],
}, otoroshi.list);

// CONSOLE COMMAND
const consoleCommand = cliparse.command('console', {
description: 'Open an application in the Console',
Expand Down Expand Up @@ -1196,6 +1251,7 @@ async function run () {
if (featuresFromConf.operators) {
commands.push(colorizeExperimentalCommand(keycloakCommand, 'operators'));
commands.push(colorizeExperimentalCommand(metabaseCommand, 'operators'));
commands.push(colorizeExperimentalCommand(otoroshiCommand, 'operators'));
}

if (featuresFromConf.kv) {
Expand Down
223 changes: 223 additions & 0 deletions src/commands/otoroshi.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import openPage from 'open';
import colors from 'colors/safe.js';

import * as NG from '../models/ng.js';
import * as Otoroshi from '../models/otoroshi.js';
import * as Operator from '../models/operators.js';

import { Logger } from '../logger.js';
import { select } from '@inquirer/prompts';
import { resolveId } from '../models/application.js';

/** Check the version of an Otoroshi operator
* @param {object} params The command's parameters
* @param {string} params.args[0] The operator's name or ID
* @param {string} params.options.format The output format
* @returns {Promise<void>}
* @throws {Error} If the operator name or ID is missing
*/
export async function checkVersion (params) {
const [addonIdOrName] = params.args;
const { format } = params.options;

const name = addonIdOrName.addon_name || addonIdOrName.realId || addonIdOrName;

if (!name) {
throw new Error('You must provide an operator name or ID');
}

const version = await Operator.checkVersion('otoroshi', addonIdOrName);

switch (format) {
case 'json':
Logger.printJson(version);
break;
case 'human':
default:
if (!version.needUpdate) {
Logger.println(`${colors.green('✔')} Otoroshi operator ${colors.green(name)} is up to date`);
}
else {
Logger.println(`🔄 Otoroshi operator ${colors.red(name)} is outdated`);
Logger.println(` ├─ Installed version: ${colors.red(version.installed)}`);
Logger.println(` └─ Latest version: ${colors.green(version.latest)}`);
Logger.println();
select({
message: `Do you want to update it to ${colors.green(version.latest)} now?`,
choices: ['Yes', 'No'],
}).then(async (answer) => {
if (answer === 'Yes') {
await Operator.updateVersion('otoroshi', addonIdOrName, version.latest);
Logger.println(colors.green('✔'), 'Your Otoroshi operator is up-to-date and being rebuilt…');
}
});
}
break;
}
}

/** Get the details of an Otoroshi operator
* @param {object} params The command's parameters
* @param {string} params.args[0] The operator's name or ID
* @param {string} params.options.format The output format
* @returns {Promise<void>}
*/
export async function get (params) {
const [addonIdOrName] = params.args;
const { format } = params.options;

const otoroshi = await Operator.getDetails('otoroshi', addonIdOrName);
printOtoroshi(otoroshi, format);
}

export async function link (params) {
const [appIdOrName, addonIdOrName] = params.args;
const otoroshi = await get(null, addonIdOrName);
const otoroshiApp = await get(otoroshi);

const { appId } = await resolveId(appIdOrName);

// Create a network group
const { id: ngId } = await NG.create(null, `${otoroshi.realId}.${appId}`, null, null, [appId, otoroshiApp.appId]);

// Get the application's member domain
const memberDomain = `${appId}.m.${ngId}.ng.${NG.DOMAIN}`;

// Create a route to the application, without TLS, on port 4242
await Otoroshi.createOtoroshiRoute(otoroshi, otoroshiApp, {
name: otoroshi.name,
enabled: true,
frontend: {
domains: [memberDomain],
},
backend: {
targets: [{
id: appId,
tls: false,
}],
root: '/',
},
plugins: [],
});
// Add basic auth to the route

Logger.println(`${colors.green('✔')} Application ${colors.green(appIdOrName.app_id || appIdOrName.app_name)} linked to Otoroshi service ${colors.green(otoroshi.name)}`);
}

/**
* List all Otoroshi operators
* @returns {Promise<void>}
*/
export async function list () {
const deployed = await Operator.listDeployed('otoroshi');

if (deployed.length === 0) {
Logger.println(`🔎 No Otoroshi operator found, create one with ${colors.blue('clever otoroshi create')} command`);
return;
}

Logger.println(`🔎 Found ${deployed.length} Otoroshi operator${deployed.length > 1 ? 's' : ''}:`);
Logger.println(deployed.map((otoroshi) => colors.grey(` • ${otoroshi.name} (${otoroshi.realId})`)).join('\n'));
}

export async function getRoutes (params) {
const [addonIdOrName] = params.args;
const { format } = params.options;

const routes = await Otoroshi.getOtoroshiRoutes(addonIdOrName);

switch (format) {
case 'json':
Logger.println(JSON.stringify(routes, null, 2));
break;
case 'human':
default:
routes.forEach((route) => {
const toPrint = {
id: route.id,
name: route.name,
enabled: route.enabled,
frontend: route.frontend.domains[0],
backend: route.backend.targets[0].id,
root: route.backend.root,
tls: route.backend.targets[0].tls,
plugins: route.plugins.length,
};
console.table(toPrint);
});
break;
}
}

/** Open an Otoroshi operator in the browser
* @param {object} params The command's parameters
* @param {string} params.args[0] The operator's name or ID
* @returns {Promise<void>}
*/
export async function open (params) {
const [addonIdOrName] = params.args;
const otoroshi = await Operator.getDetails('otoroshi', addonIdOrName);

Logger.println(`Opening Otoroshi operator ${colors.blue(otoroshi.addonId)} in the browser…`);
await openPage(`https://${otoroshi.adminTargethost}`, { wait: false });
}

/** Open the Logs section of an Otoroshi Operator application in the Clever Cloud Console
* @param {object} params The command's parameters
* @param {string} params.args[0] The operator's name or ID
* @returns {Promise<void>}
*/
export async function openLogs (params) {
const [addonIdOrName] = params.args;
const otoroshi = await Operator.getDetails('otoroshi', addonIdOrName);

Logger.println(`Opening Otoroshi operator logs ${colors.blue(otoroshi.addonId)} in the Clever Cloud Console…`);
await openPage(`https://console.clever-cloud.com/organisations/${otoroshi.ownerId}/applications/${otoroshi.applications[0].javaId}/logs`, { wait: false });
}

/** Reboot an Otoroshi operator
* @param {object} params The command's parameters
* @param {string} params.args[0] The operator's name or ID
* @returns {Promise<void>}
*/
export async function reboot (params) {
const [addonIdOrName] = params.args;

await Operator.reboot('otoroshi', addonIdOrName);
Logger.println(`🔄 Rebooting Otoroshi operator ${colors.blue(addonIdOrName.addon_name)}…`);

}

/** Rebuild an Otoroshi operator
* @param {object} params The command's parameters
* @param {string} params.args[0] The operator's name or ID
* @returns {Promise<void>}
*/
export async function rebuild (params) {
const [addonIdOrName] = params.args;

await Operator.rebuild('otoroshi', addonIdOrName);
Logger.println(`🔄 Rebuilding Otoroshi operator ${colors.blue(addonIdOrName.addon_name)}…`);
}

/** Print the details of a Otoroshi operator
* @param {object} otoroshi The Otoroshi operator object
* @param {string} [format] The output format
* @returns {void}
*/
function printOtoroshi (otoroshi, format = 'human') {
switch (format) {
case 'json':
Logger.println(JSON.stringify(otoroshi, null, 2));
break;
case 'human':
default:
console.table({
// Name: otoroshi.name,
ID: otoroshi.addonId,
'Admin URL': `https://${otoroshi.adminTargethost}`,
'API URL': `https://${otoroshi.apiHost}`,
});
break;
}
}
49 changes: 49 additions & 0 deletions src/models/otoroshi-api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// TODO: Move this to the Clever Cloud JS Client

/**
* GET /v4/addon-providers/addon-otoroshi/addons/{otoroshiId}
* @param {Object} params
* @param {String} params.undefined
*/
export function getOtoroshi (params) {
// no multipath for /self or /organisations/{id}
return Promise.resolve({
method: 'get',
url: `/v4/addon-providers/addon-otoroshi/addons/${params.otoroshiId}`,
headers: { Accept: 'application/json' },
// no queryParams
// no body
});
}

/**
* POST /v4/addon-providers/addon-otoroshi/addons/{otoroshiId}/reboot
* @param {Object} params
* @param {String} params.undefined
*/
export function rebootOtoroshi (params) {
// no multipath for /self or /organisations/{id}/ng
return Promise.resolve({
method: 'post',
url: `/v4/addon-providers/addon-otoroshi/addons/${params.otoroshiId}/reboot`,
headers: { Accept: 'application/json' },
// no queryParams
// no body
});
}

/**
* POST /v4/addon-providers/addon-otoroshi/addons/{otoroshiId}/rebuild
* @param {Object} params
* @param {String} params.undefined
*/
export function rebuildOtoroshi (params) {
// no multipath for /self or /organisations/{id}/ng
return Promise.resolve({
method: 'post',
url: `/v4/addon-providers/addon-otoroshi/addons/${params.otoroshiId}/rebuild`,
headers: { Accept: 'application/json' },
// no queryParams
// no body
});
}
Loading

0 comments on commit a101b70

Please sign in to comment.