Skip to content

Commit

Permalink
fix(redis-v5): use billing entity to look up config vars (#1572)
Browse files Browse the repository at this point in the history
  • Loading branch information
fivetanley authored Jul 30, 2020
1 parent 0ad377a commit 8ee4bb9
Show file tree
Hide file tree
Showing 2 changed files with 136 additions and 19 deletions.
19 changes: 13 additions & 6 deletions packages/redis-v5/commands/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,7 @@ module.exports = {
run: cli.command({ preauth: true }, async (context, heroku) => {
const api = require('../lib/shared')(context, heroku)
let addon = await api.getRedisAddon()

let config = await heroku.get(`/apps/${context.app}/config-vars`)
let configVars = await getRedisConfigVars(addon, heroku)

let redis = await api.request(`/redis/v0/databases/${addon.name}`)
let hobby = redis.plan.indexOf('hobby') === 0
Expand All @@ -186,13 +185,21 @@ module.exports = {
await cli.confirmApp(context.app, context.flags.confirm, 'WARNING: Insecure action.\nAll data, including the Redis password, will not be encrypted.')
}

let vars = {}
addon.config_vars.forEach(function (key) { vars[key] = config[key] })
let nonBastionVars = addon.config_vars.filter(function (configVar) {
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, vars)
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
}, {})
}
136 changes: 123 additions & 13 deletions packages/redis-v5/test/commands/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@ let expect = require('chai').expect
let Duplex = require('stream').Duplex
let EventEmitter = require('events').EventEmitter

let command, net, tls
let command, net, tls, tunnel

describe('heroku redis:cli', function () {
let command = proxyquire('../../commands/cli.js', { net: {}, tls: {}, ssh2: {} })
require('../lib/shared').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()

Expand All @@ -33,12 +36,13 @@ describe('heroku redis:cli', function () {
}

class Tunnel extends EventEmitter {
connect () {
this.emit('ready')
}

forwardOut (localHost, localPort, hostname, port, cb) {
cb(null, new Client())
constructor () {
super()
tunnel = this
this.forwardOut = sinon.stub().yields(null, new Client())
this.connect = sinon.stub().callsFake(() => {
this.emit('ready')
})
}
}

Expand All @@ -50,7 +54,16 @@ describe('heroku redis:cli', function () {
it('# for hobby it uses net.connect', function () {
let app = nock('https://api.heroku.com:443')
.get('/apps/example/addons').reply(200, [
{ name: 'redis-haiku', addon_service: { name: 'heroku-redis' }, config_vars: ['REDIS_FOO', 'REDIS_BAR'] }
{
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')
Expand All @@ -73,7 +86,16 @@ describe('heroku redis:cli', function () {
it('# for premium it uses tls.connect', function () {
let app = nock('https://api.heroku.com:443')
.get('/apps/example/addons').reply(200, [
{ name: 'redis-haiku', addon_service: { name: 'heroku-redis' }, config_vars: ['REDIS_FOO', 'REDIS_BAR'] }
{
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')
Expand All @@ -97,9 +119,16 @@ describe('heroku redis:cli', function () {
it('# for bastion it uses tunnel.connect', function () {
let app = nock('https://api.heroku.com:443')
.get('/apps/example/addons').reply(200, [
{ name: 'redis-haiku',
{
id: addonId,
name: 'redis-haiku',
addon_service: { name: 'heroku-redis' },
config_vars: ['REDIS_URL', 'REDIS_BASTIONS', 'REDIS_BASTION_KEY', 'REDIS_BASTION_REKEYS_AFTER'] }
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')
Expand All @@ -122,9 +151,15 @@ describe('heroku redis:cli', function () {
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, [
{ name: 'redis-haiku',
{ id: addonId,
name: 'redis-haiku',
addon_service: { name: 'heroku-redis' },
config_vars: ['REDIS_URL', 'REDIS_BASTIONS', 'REDIS_BASTION_KEY', 'REDIS_BASTION_REKEYS_AFTER'] }
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')
Expand All @@ -145,4 +180,79 @@ describe('heroku redis:cli', function () {
.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': '[email protected]',
'REDIS_6_BASTIONS': 'redis-6-bastion.example.com',
'REDIS_6_BASTION_KEY': 'key2'
})

let redis = nock('https://redis-api.heroku.com:443')
.get('/redis/v0/databases/redis-sonnet').reply(200, {
resource_url: 'redis://foobar:[email protected]: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
})
})
})

0 comments on commit 8ee4bb9

Please sign in to comment.