From ce2f9b604e202f12dce30cac87941b5b10cb2e05 Mon Sep 17 00:00:00 2001 From: Justin Wilaby Date: Mon, 18 Mar 2024 15:00:17 -0700 Subject: [PATCH] removed old command --- packages/cli/src/commands/redis/cli.ts | 8 +- packages/cli/src/lib/redis/utils.ts | 1 - packages/redis-v5/commands/cli.js | 232 ------------ packages/redis-v5/index.js | 1 - .../test/unit/commands/cli.unit.test.js | 337 ------------------ 5 files changed, 3 insertions(+), 576 deletions(-) delete mode 100644 packages/redis-v5/commands/cli.js delete mode 100644 packages/redis-v5/test/unit/commands/cli.unit.test.js diff --git a/packages/cli/src/commands/redis/cli.ts b/packages/cli/src/commands/redis/cli.ts index 8b5682b8f0..f05936c219 100644 --- a/packages/cli/src/commands/redis/cli.ts +++ b/packages/cli/src/commands/redis/cli.ts @@ -6,8 +6,6 @@ import {Client} from 'ssh2' import Parser = require('redis-parser') import type {Writable} from 'node:stream' import portfinder = require('portfinder') -import {ClientRequestArgs} from 'node:http' -import {urlToHttpOptions} from 'url' import {getRedisAddon, getRedisFormation, RedisFormation} from '../../lib/redis/utils' import confirmApp from '../../lib/apps/confirm-app' import * as tls from 'tls' @@ -18,7 +16,7 @@ import * as net from 'net' const REPLY_OK = 'OK' -async function redisCLI(uri: ClientRequestArgs, client: Writable): Promise { +async function redisCLI(uri: URL, client: Writable): Promise { const io = readline.createInterface(process.stdin, process.stdout) const reply = new Parser({ returnReply(reply) { @@ -74,7 +72,7 @@ async function redisCLI(uri: ClientRequestArgs, client: Writable): Promise }, }) let state = 'connect' - client.write(`AUTH ${uri.auth?.split(':')[1]}\n`) + client.write(`AUTH ${uri.password}\n`) io.setPrompt(uri.host + '> ') io.on('line', function (line) { switch (line.split(' ')[0]) { @@ -133,7 +131,7 @@ async function bastionConnect(uri: URL, bastions: string, config: Record tunnel.end()) stream.on('end', () => client.end()) - return redisCLI(urlToHttpOptions(uri), client) + return redisCLI(uri, client) } function match(config: Record, lookup: RegExp): string | null { diff --git a/packages/cli/src/lib/redis/utils.ts b/packages/cli/src/lib/redis/utils.ts index a2359205ce..e05864911d 100644 --- a/packages/cli/src/lib/redis/utils.ts +++ b/packages/cli/src/lib/redis/utils.ts @@ -1,4 +1,3 @@ -'use strict' import {AddOn} from '@heroku-cli/schema' import {APIClient} from '@heroku-cli/command' import {HTTP} from 'http-call' diff --git a/packages/redis-v5/commands/cli.js b/packages/redis-v5/commands/cli.js deleted file mode 100644 index 266e0f6973..0000000000 --- a/packages/redis-v5/commands/cli.js +++ /dev/null @@ -1,232 +0,0 @@ -'use strict' - -let cli = require('heroku-cli-util') -let net = require('net') -let Parser = require('redis-parser') -let readline = require('readline') -let tls = require('tls') -let url = require('url') -let Client = require('ssh2').Client - -const REPLY_OK = 'OK' - -function redisCLI(uri, client) { - let io = readline.createInterface(process.stdin, process.stdout) - let reply = new Parser({ - returnReply(reply) { - switch (state) { - case 'monitoring': - if (reply !== REPLY_OK) { - console.log(reply) - } - - break - case 'subscriber': - if (Array.isArray(reply)) { - reply.forEach(function (value, i) { - console.log(`${i + 1}) ${value}`) - }) - } else { - console.log(reply) - } - - break - case 'connect': - if (reply !== REPLY_OK) { - console.log(reply) - } - - state = 'normal' - io.prompt() - break - case 'closing': - if (reply !== REPLY_OK) { - console.log(reply) - } - - break - default: - if (Array.isArray(reply)) { - reply.forEach(function (value, i) { - console.log(`${i + 1}) ${value}`) - }) - } else { - console.log(reply) - } - - io.prompt() - break - } - }, - returnError(err) { - console.log(err.message) - io.prompt() - }, - returnFatalError(err) { - client.emit('error', err) - console.dir(err) - }, - }) - let state = 'connect' - - client.write(`AUTH ${uri.auth.split(':')[1]}\n`) - - io.setPrompt(uri.host + '> ') - io.on('line', function (line) { - switch (line.split(' ')[0]) { - case 'MONITOR': - state = 'monitoring' - break - case 'PSUBSCRIBE': - case 'SUBSCRIBE': - state = 'subscriber' - break - } - - client.write(`${line} -`) - }) - io.on('close', function () { - state = 'closing' - client.write('QUIT\n') - }) - client.on('data', function (data) { - reply.execute(data) - }) - return new Promise((resolve, reject) => { - client.on('error', reject) - client.on('end', function () { - console.log('\nDisconnected from instance.') - io.close() - resolve() - }) - }) -} - -function bastionConnect({uri, bastions, config, prefer_native_tls}) { - return new Promise((resolve, reject) => { - let tunnel = new Client() - tunnel.on('ready', function () { - // eslint-disable-next-line no-mixed-operators - let localPort = Math.floor(Math.random() * (65535 - 49152) + 49152) - tunnel.forwardOut('localhost', localPort, uri.hostname, uri.port, function (err, stream) { - if (err) return reject(err) - stream.on('close', () => tunnel.end()) - stream.on('end', () => client.end()) - - let client - if (prefer_native_tls) { - client = tls.connect({ - socket: stream, - port: Number.parseInt(uri.port, 10), - host: uri.hostname, - rejectUnauthorized: false, - }) - } else { - client = stream - } - - redisCLI(uri, client).then(resolve).catch(reject) - }) - }).connect({ - host: bastions.split(',')[0], - username: 'bastion', - privateKey: match(config, /_BASTION_KEY/), - }) - }) -} - -function match(config, lookup) { - for (var key in config) { - if (lookup.test(key)) { - return config[key] - } - } - - return null -} - -function maybeTunnel(redis, config) { - let bastions = match(config, /_BASTIONS/) - let hobby = redis.plan.indexOf('hobby') === 0 - let uri = url.parse(redis.resource_url) - let prefer_native_tls = redis.prefer_native_tls - - if (prefer_native_tls && hobby) { - uri = url.parse(match(config, /_TLS_URL/)) - } - - // eslint-disable-next-line no-negated-condition, no-eq-null, eqeqeq - if (bastions != null) { - return bastionConnect({uri, bastions, config, prefer_native_tls}) - // eslint-disable-next-line no-else-return - } else { - let client - if (prefer_native_tls) { - client = tls.connect({ - port: Number.parseInt(uri.port, 10), - host: uri.hostname, - rejectUnauthorized: false, - }) - } else if (!hobby) { - client = tls.connect({ - port: Number.parseInt(uri.port, 10) + 1, - host: uri.hostname, - rejectUnauthorized: false, - }) - } else { - client = net.connect({port: uri.port, host: uri.hostname}) - } - - return redisCLI(uri, client) - } -} - -module.exports = { - topic: 'redis', - command: 'cli', - needsApp: true, - needsAuth: true, - description: 'opens a redis prompt', - args: [{name: 'database', optional: true}], - flags: [{name: 'confirm', char: 'c', hasValue: true}], - run: cli.command({preauth: true}, async (context, heroku) => { - const api = require('../lib/shared')(context, heroku) - let addon = await api.getRedisAddon() - let configVars = await getRedisConfigVars(addon, heroku) - - let redis = await api.request(`/redis/v0/databases/${addon.name}`) - - if (redis.plan.startsWith('shield-')) { - cli.error(` - Using redis:cli on Heroku Redis shield plans is not supported. - Please see Heroku DevCenter for more details: https://devcenter.heroku.com/articles/shield-private-space#shield-features - `) - cli.exit(1) - } - - let hobby = redis.plan.indexOf('hobby') === 0 - let prefer_native_tls = redis.prefer_native_tls - - if (!prefer_native_tls && hobby) { - await cli.confirmApp(context.app, context.flags.confirm, 'WARNING: Insecure action.\nAll data, including the Redis password, will not be encrypted.') - } - - let nonBastionVars = Object.keys(configVars).filter(function (configVar) { - return !(/(?:BASTIONS|BASTION_KEY|BASTION_REKEYS_AFTER)$/.test(configVar)) - }).join(', ') - - cli.log(`Connecting to ${addon.name} (${nonBastionVars}):`) - return maybeTunnel(redis, configVars) - }), -} - -// try to lookup the right config vars from the billing app -async function getRedisConfigVars(addon, heroku) { - let config = await heroku.get(`/apps/${addon.billing_entity.name}/config-vars`) - - return addon.config_vars.reduce((memo, configVar) => { - memo[configVar] = config[configVar] - return memo - }, {}) -} diff --git a/packages/redis-v5/index.js b/packages/redis-v5/index.js index 757bcbea9b..c82cf7e125 100644 --- a/packages/redis-v5/index.js +++ b/packages/redis-v5/index.js @@ -1,5 +1,4 @@ exports.commands = [ - require('./commands/cli'), require('./commands/wait'), require('./commands/promote'), require('./commands/timeout'), diff --git a/packages/redis-v5/test/unit/commands/cli.unit.test.js b/packages/redis-v5/test/unit/commands/cli.unit.test.js deleted file mode 100644 index 8c80600c9b..0000000000 --- a/packages/redis-v5/test/unit/commands/cli.unit.test.js +++ /dev/null @@ -1,337 +0,0 @@ -'use strict' -/* globals beforeEach cli */ - -let nock = require('nock') -let sinon = require('sinon') -let proxyquire = require('proxyquire').noCallThru() -let expect = require('chai').expect -let Duplex = require('stream').Duplex -let EventEmitter = require('events').EventEmitter - -let command -let net -let tls -let tunnel - -describe('heroku redis:cli', function () { - let command = proxyquire('../../../commands/cli.js', {net: {}, tls: {}, ssh2: {}}) - require('../lib/shared.unit.test').shouldHandleArgs(command) -}) - -describe('heroku redis:cli', function () { - const addonId = '1dcb269b-8be5-4132-8aeb-e3f3c7364958' - const appId = '7b0ae612-8775-4502-a5b5-2b45a4d18b2d' - - beforeEach(function () { - cli.mockConsole() - cli.exit.mock() - - class Client extends Duplex { - _write(chunk, encoding, callback) {} - _read(size) { - this.emit('end') - } - } - - net = { - connect: sinon.stub().returns(new Client()), - } - - tls = { - connect: sinon.stub().returns(new Client()), - } - - class Tunnel extends EventEmitter { - constructor() { - super() - // eslint-disable-next-line unicorn/no-this-assignment - tunnel = this - this.forwardOut = sinon.stub().yields(null, new Client()) - this.connect = sinon.stub().callsFake(() => { - this.emit('ready') - }) - this.end = sinon.stub() - } - } - - let ssh2 = {Client: Tunnel} - - command = proxyquire('../../../commands/cli.js', {net, tls, ssh2}) - }) - - it('# for hobby it uses net.connect', function () { - let app = nock('https://api.heroku.com:443') - .get('/apps/example/addons').reply(200, [ - { - id: addonId, - name: 'redis-haiku', - addon_service: {name: 'heroku-redis'}, - config_vars: ['REDIS_FOO', 'REDIS_BAR'], - billing_entity: { - id: appId, - name: 'example', - }, - }, - ]) - - let configVars = nock('https://api.heroku.com:443') - .get('/apps/example/config-vars').reply(200, {FOO: 'BAR'}) - - let redis = nock('https://api.data.heroku.com:443') - .get('/redis/v0/databases/redis-haiku').reply(200, { - resource_url: 'redis://foobar:password@example.com:8649', - plan: 'hobby', - }) - return command.run({app: 'example', flags: {confirm: 'example'}, args: {}, auth: {username: 'foobar', password: 'password'}}) - .then(() => app.done()) - .then(() => configVars.done()) - .then(() => redis.done()) - .then(() => expect(cli.stdout).to.equal('Connecting to redis-haiku (REDIS_FOO, REDIS_BAR):\n')) - .then(() => expect(cli.stderr).to.equal('')) - .then(() => expect(net.connect.called).to.equal(true)) - }) - - it('# for hobby it uses TLS if prefer_native_tls', function () { - let app = nock('https://api.heroku.com:443') - .get('/apps/example/addons').reply(200, [ - { - id: addonId, - name: 'redis-haiku', - addon_service: {name: 'heroku-redis'}, - config_vars: ['REDIS_FOO', 'REDIS_BAR', 'REDIS_TLS_URL'], - billing_entity: { - id: appId, - name: 'example', - }, - }, - ]) - - let configVars = nock('https://api.heroku.com:443') - .get('/apps/example/config-vars').reply(200, {REDIS_TLS_URL: 'rediss://foobar:password@example.com:8649'}) - - let redis = nock('https://api.data.heroku.com:443') - .get('/redis/v0/databases/redis-haiku').reply(200, { - resource_url: 'redis://foobar:password@example.com:8649', - plan: 'hobby', - prefer_native_tls: true, - }) - return command.run({app: 'example', flags: {confirm: 'example'}, args: {}, auth: {username: 'foobar', password: 'password'}}) - .then(() => app.done()) - .then(() => configVars.done()) - .then(() => redis.done()) - .then(() => expect(cli.stdout).to.equal('Connecting to redis-haiku (REDIS_FOO, REDIS_BAR, REDIS_TLS_URL):\n')) - .then(() => expect(cli.stderr).to.equal('')) - .then(() => expect(tls.connect.called).to.equal(true)) - }) - - it('# for premium it uses tls.connect', function () { - let app = nock('https://api.heroku.com:443') - .get('/apps/example/addons').reply(200, [ - { - id: addonId, - name: 'redis-haiku', - addon_service: {name: 'heroku-redis'}, - config_vars: ['REDIS_FOO', 'REDIS_BAR'], - billing_entity: { - id: appId, - name: 'example', - }, - }, - ]) - - let configVars = nock('https://api.heroku.com:443') - .get('/apps/example/config-vars').reply(200, {FOO: 'BAR'}) - - let redis = nock('https://api.data.heroku.com:443') - .get('/redis/v0/databases/redis-haiku').reply(200, { - resource_url: 'redis://foobar:password@example.com:8649', - plan: 'premium-0', - }) - - return command.run({app: 'example', flags: {confirm: 'example'}, args: {}, auth: {username: 'foobar', password: 'password'}}) - .then(() => app.done()) - .then(() => configVars.done()) - .then(() => redis.done()) - .then(() => expect(cli.stdout).to.equal('Connecting to redis-haiku (REDIS_FOO, REDIS_BAR):\n')) - .then(() => expect(cli.stderr).to.equal('')) - .then(() => expect(tls.connect.called).to.equal(true)) - }) - - it('# exits with an error with shield databases', async function () { - let app = nock('https://api.heroku.com:443') - .get('/apps/example/addons').reply(200, [ - { - id: addonId, - name: 'redis-haiku', - addon_service: {name: 'heroku-redis'}, - config_vars: ['REDIS_FOO', 'REDIS_BAR'], - billing_entity: { - id: appId, - name: 'example', - }, - }, - ]) - - let configVars = nock('https://api.heroku.com:443') - .get('/apps/example/config-vars').reply(200, {FOO: 'BAR'}) - - let redis = nock('https://api.data.heroku.com:443') - .get('/redis/v0/databases/redis-haiku').reply(200, { - resource_url: 'redis://foobar:password@example.com:8649', - plan: 'shield-9', - }) - - try { - await command.run({app: 'example', flags: {confirm: 'example'}, args: {}, auth: {username: 'foobar', password: 'password'}}) - expect(true, 'cli command should fail!').to.equal(false) - } catch (error) { - expect(error).to.be.an.instanceof(cli.exit.ErrorExit) - expect(error.code).to.equal(1) - } - - await app.done() - await redis.done() - await configVars.done() - expect(cli.stderr).to.contain('Using redis:cli on Heroku Redis shield plans is not supported.') - }) - - it('# for bastion it uses tunnel.connect', function () { - let app = nock('https://api.heroku.com:443') - .get('/apps/example/addons').reply(200, [ - { - id: addonId, - name: 'redis-haiku', - addon_service: {name: 'heroku-redis'}, - config_vars: ['REDIS_URL', 'REDIS_BASTIONS', 'REDIS_BASTION_KEY', 'REDIS_BASTION_REKEYS_AFTER'], - billing_entity: { - id: appId, - name: 'example', - }, - }, - ]) - - let configVars = nock('https://api.heroku.com:443') - .get('/apps/example/config-vars').reply(200, {REDIS_BASTIONS: 'example.com'}) - - let redis = nock('https://api.data.heroku.com:443') - .get('/redis/v0/databases/redis-haiku').reply(200, { - resource_url: 'redis://foobar:password@example.com:8649', - plan: 'premium-0', - }) - - return command.run({app: 'example', flags: {confirm: 'example'}, args: {}, auth: {username: 'foobar', password: 'password'}}) - .then(() => app.done()) - .then(() => configVars.done()) - .then(() => redis.done()) - .then(() => expect(cli.stdout).to.equal('Connecting to redis-haiku (REDIS_URL):\n')) - .then(() => expect(cli.stderr).to.equal('')) - }) - - it('# for private spaces bastion with prefer_native_tls, it uses tls.connect', function () { - let app = nock('https://api.heroku.com:443') - .get('/apps/example/addons').reply(200, [ - {id: addonId, - name: 'redis-haiku', - addon_service: {name: 'heroku-redis'}, - config_vars: ['REDIS_URL', 'REDIS_BASTIONS', 'REDIS_BASTION_KEY', 'REDIS_BASTION_REKEYS_AFTER'], - billing_entity: { - id: appId, - name: 'example', - }, - }, - ]) - - let configVars = nock('https://api.heroku.com:443') - .get('/apps/example/config-vars').reply(200, {REDIS_BASTIONS: 'example.com'}) - - let redis = nock('https://api.data.heroku.com:443') - .get('/redis/v0/databases/redis-haiku').reply(200, { - resource_url: 'redis://foobar:password@example.com:8649', - plan: 'private-7', - prefer_native_tls: true, - }) - - return command.run({app: 'example', flags: {confirm: 'example'}, args: {}, auth: {username: 'foobar', password: 'password'}}) - .then(() => app.done()) - .then(() => configVars.done()) - .then(() => redis.done()) - .then(() => expect(cli.stdout).to.equal('Connecting to redis-haiku (REDIS_URL):\n')) - .then(() => expect(cli.stderr).to.equal('')) - .then(() => expect(tls.connect.called).to.equal(true)) - }) - - it('# selects correct connection information when multiple redises are present across multiple apps', async () => { - let app = nock('https://api.heroku.com:443') - .get('/apps/example/addons').reply(200, [ - { - id: addonId, - name: 'redis-haiku', - addon_service: {name: 'heroku-redis'}, - config_vars: ['REDIS_URL', 'REDIS_BASTIONS', 'REDIS_BASTION_KEY', 'REDIS_BASTION_REKEYS_AFTER'], - billing_entity: { - id: appId, - name: 'example', - }, - }, - { - id: 'heroku-redis-addon-id-2', - name: 'redis-sonnet', - addon_service: {name: 'heroku-redis'}, - config_vars: ['REDIS_6_URL', 'REDIS_6_BASTIONS', 'REDIS_6_BASTION_KEY', 'REDIS_6_BASTION_REKEYS_AFTER'], - billing_entity: { - id: 'app-2-id', - name: 'example-app-2', - }, - }, - ]) - - let configVars = nock('https://api.heroku.com:443') - .get('/apps/example-app-2/config-vars').reply(200, { - REDIS_6_URL: 'redis-user@redis6-example.com', - REDIS_6_BASTIONS: 'redis-6-bastion.example.com', - REDIS_6_BASTION_KEY: 'key2', - }) - - let redis = nock('https://api.data.heroku.com:443') - .get('/redis/v0/databases/redis-sonnet').reply(200, { - resource_url: 'redis://foobar:password@redis-6.example.com:8649', - plan: 'private-7', - prefer_native_tls: true, - }) - - await command.run({app: 'example', flags: {confirm: 'example'}, args: {database: 'redis-sonnet'}, auth: {username: 'foobar', password: 'password'}}) - app.done() - configVars.done() - redis.done() - - expect(cli.stdout).to.equal('Connecting to redis-sonnet (REDIS_6_URL):\n') - expect(cli.stderr).to.equal('') - - const connectArgs = tunnel.connect.args[0] - expect(connectArgs).to.have.length(1) - expect(connectArgs[0]).to.deep.equal({ - host: 'redis-6-bastion.example.com', - privateKey: 'key2', - username: 'bastion', - }) - - const tunnelArgs = tunnel.forwardOut.args[0] - const [localAddr, localPort, remoteAddr, remotePort] = tunnelArgs - expect(localAddr).to.equal('localhost') - expect(localPort).to.be.a('number') - expect(remoteAddr).to.equal('redis-6.example.com') - expect(remotePort).to.equal('8649') - - const tlsConnectArgs = tls.connect.args[0] - expect(tlsConnectArgs).to.have.length(1) - const tlsConnectOptions = { - ...tlsConnectArgs[0], - } - delete tlsConnectOptions.socket - expect(tlsConnectOptions).to.deep.equal({ - port: 8649, - host: 'redis-6.example.com', - rejectUnauthorized: false, - }) - }) -})