Skip to content
Draft
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
34 changes: 30 additions & 4 deletions bin/clever.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 kms from '../src/commands/kms.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';
Expand Down Expand Up @@ -92,6 +93,8 @@ async function run () {

// ARGUMENTS
const args = {
kmsKeyValue: cliparse.argument('key-value', { description: 'A key/value to store in a Clever KMS secret (e.g. secretKey=secretValue), can be used multiple times' }),
kmsSecret: cliparse.argument('secret', { description: 'The secret to get from Clever KMS' }),
kvRawCommand: cliparse.argument('command', { description: 'The raw command to send to the Materia KV or Redis® add-on' }),
kvIdOrName: cliparse.argument('kv-id', {
description: 'Add-on/Real ID (or name, if unambiguous) of a Materia KV or Redis® add-on',
Expand Down Expand Up @@ -774,6 +777,25 @@ async function run () {
commands: [enableFeatureCommand, disableFeatureCommand, listFeaturesCommand, infoFeaturesCommand],
}, features.list);

// KMS COMMANDS
const kmsGetCommand = cliparse.command('get', {
description: 'Get the value of a secret',
args: [args.kmsSecret],
}, kms.get);
const kmsPatchCommand = cliparse.command('patch', {
description: 'Patch an existing secret',
args: [args.kmsSecret, args.kmsKeyValue],
}, kms.patch);
const kmsPutCommand = cliparse.command('put', {
description: 'Set the value of a secret',
args: [args.kmsSecret, args.kmsKeyValue],
}, kms.put);
const kmsCommands = cliparse.command('kms', {
description: 'Manage secrets',
options: [opts.humanJsonOutputFormat],
commands: [kmsGetCommand, kmsPatchCommand, kmsPutCommand],
}, kms.get);

// KV COMMAND
const kvRawCommand = cliparse.command('kv', {
description: 'Send a raw command to a Materia KV or Redis® add-on',
Expand Down Expand Up @@ -1109,18 +1131,22 @@ async function run () {
// Add experimental features only if they are enabled through the configuration file
const featuresFromConf = await getFeatures();

if (featuresFromConf.kv) {
commands.push(colorizeExperimentalCommand(kvRawCommand, 'kv'));
if (featuresFromConf.kms) {
commands.push(colorizeExperimentalCommand(kmsCommands, 'kms'));
}

if (featuresFromConf.tokens) {
commands.push(colorizeExperimentalCommand(tokensCommands, 'tokens'));
if (featuresFromConf.kv) {
commands.push(colorizeExperimentalCommand(kvRawCommand, 'kv'));
}

if (featuresFromConf.ng) {
commands.push(colorizeExperimentalCommand(networkGroupsCommand, 'ng'));
}

if (featuresFromConf.tokens) {
commands.push(colorizeExperimentalCommand(tokensCommands, 'tokens'));
}

// CLI PARSER
const cliParser = cliparse.cli({
name: 'clever',
Expand Down
73 changes: 73 additions & 0 deletions src/commands/kms.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Logger } from '../logger.js';
import { getSecret, patchSecret, putSecret } from '../models/kms.js';

/**
* Get secret from Clever KMS
* @param {*} params
* @param {string} params.args[0] - secret name
* @param {object} params.options.format - output format
* @returns {Promise<void>}
*/
export async function get (params) {
const [fieldKey] = params.args;
const { format } = params.options;

const secret = await getSecret(fieldKey);

switch (format) {
case 'json':
Logger.printJson(secret.data.data);
break;
case 'human':
default:
console.table(secret.data.data);
}
}

/**
* Patch secret in Clever KMS
* @param {object} params
* @param {string} params.args[0] - secret name
* @param {Array<string>} params.args[...n] - key=value pairs
* @param {object} params.options.format - output format
* @returns {Promise<void>}
*/
export async function patch (params) {
const [fieldKey, ...fieldValues] = params.args;
const { format } = params.options;

const response = await patchSecret(fieldKey, fieldValues);

switch (format) {
case 'json':
Logger.printJson(response.data);
break;
case 'human':
default:
console.table(response.data);
}
}

/**
* Put secret in Clever KMS
* @param {object} params
* @param {string} params.args[0] - secret name
* @param {Array<string>} params.args[...n] - key=value pairs
* @param {object} params.options.format - output format
* @returns {Promise<void>}
*/
export async function put (params) {
const [fieldKey, ...fieldValues] = params.args;
const { format } = params.options;

const response = await putSecret(fieldKey, fieldValues);

switch (format) {
case 'json':
Logger.printJson(response.data);
break;
case 'human':
default:
console.table(response.data);
}
}
14 changes: 14 additions & 0 deletions src/experimental-features.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@ import dedent from 'dedent';
import { conf } from './models/configuration.js';

export const EXPERIMENTAL_FEATURES = {
kms: {
status: 'alpha',
description: 'Manage your secrets in Clever KMS',
instructions: `
GET and PUT secret in Clever KMS directly from Clever Tools, without other dependencies

clever kms put mySecret mySecretKey=mySecretValue
clever kms put myOtherSecret mySecretKey=mySecretValue myOtherSecretKey="$MY_ENV_VAR_SECRET_VALUE"
clever kms get mySecret
clever kms get myOtherSecret -F json

Learn more about Materia KV: https://www.clever-cloud.com/developers/doc/addons/kms
`,
},
kv: {
status: 'alpha',
description: 'Send commands to databases such as Materia KV or Redis® directly from Clever Tools, without other dependencies',
Expand Down
1 change: 1 addition & 0 deletions src/models/configuration.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ export async function setFeature (feature, value) {
export const conf = env.getOrElseAll({
API_HOST: 'https://api.clever-cloud.com',
AUTH_BRIDGE_HOST: 'https://api-bridge.clever-cloud.com',
KMS_HOST: process.env.VAULT_ADDR,
SSH_GATEWAY: 'ssh@sshgateway-clevercloud-customers.services.clever-cloud.com',

// the disclosure of these tokens is not considered as a vulnerability. Do not report this to our security service.
Expand Down
46 changes: 46 additions & 0 deletions src/models/kms-api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* GET /v1/secret/data/{secret}
* @param {Object} params
* @param {String} params.secret
*/
export function getSecret (params) {
return Promise.resolve({
method: 'get',
url: `/v1/secret/data/${params.secret}`,
headers: { Accept: 'application/json' },
// no query params
// no body
});
}

/**
* PATCH /v1/secret/data/{secret}
* @param {Object} params
* @param {String} params.secret
* @param {Object} body
*/
export function patchSecret (params, body) {
return Promise.resolve({
method: 'patch',
url: `/v1/secret/data/${params.secret}`,
headers: { 'Content-Type': 'application/json' },
body,
// no query params
});
}

/**
* PUT /v1/secret/data/{secret}
* @param {Object} params
* @param {String} params.secret
* @param {Object} body
*/
export function putSecret (params, body) {
return Promise.resolve({
method: 'post',
url: `/v1/secret/data/${params.secret}`,
headers: { 'Content-Type': 'application/json' },
body,
// no query params
});
}
87 changes: 87 additions & 0 deletions src/models/kms.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import fetch from 'node-fetch';
import colors from 'colors/safe.js';
import * as KMS from './kms-api.js';
import { conf } from './configuration.js';

/**
* Get secret from Clever KMS
* @param {string} secret
* @returns {Promise<object>} secret data
*/
export async function getSecret (secret) {
return KMS.getSecret({ secret }).then(sendToKMS);
};

/**
* Patch secret in Clever KMS
* @param {string} secret
* @param {Array<string>} data - key=value pairs
* @returns {Promise<object>} secret data
*/
export const patchSecret = (secret, data) => {
return KMS.patchSecret({ secret }, {
options: { cas: 0 },
data: checkKeyValues(data),
}).then(sendToKMS);
};

/**
* Put secret in Clever KMS
* @param {string} secret
* @param {Array<string>} data - key=value pairs
* @returns {Promise<object>} secret data
*/
export const putSecret = (secret, data) => {
return KMS.putSecret({ secret }, {
options: { cas: 0 },
data: checkKeyValues(data),
}).then(sendToKMS);
};

/**
* Check key=value pairs
* @param {Array<string>} keyValues
* @returns {object} valid key=value pairs
*/
export const checkKeyValues = (keyValues) => {
const parsed = {};

for (const kv of keyValues) {
const [key, value] = kv.split('=');
if (!value) {
console.error(`${colors.yellow('[WARNING]')} Invalid format: ${colors.yellow(kv)}, expected key=value`);
continue;
}
parsed[key] = value;
}
return parsed;
};

/**
* Send a request to Clever KMS
* @param {object} request - request object for fetch
* @returns {Promise<object>} response data
* @throws {Error} if the request fails
* @throws {Error} if the secret is not found
*/
export async function sendToKMS (request) {

request.url = `${conf.KMS_HOST}${request.url}`;
request.headers['X-Vault-Token'] = process.env.VAULT_TOKEN;

const response = await fetch(request.url, {
method: request.method,
headers: request.headers,
body: JSON.stringify(request.body),
});

if (response.status === 404) {
throw new Error(`Secret not found: ${colors.red(request.url)}`);
}

if (!response.ok) {
throw new Error(`Request failed (${response.status}): ${response.statusText}`);
}

return response.json();
};