Skip to content

Commit

Permalink
Merge pull request #105 from OldSneerJaw/data-integrations
Browse files Browse the repository at this point in the history
Support for data integrations
  • Loading branch information
OldSneerJaw authored Jan 26, 2023
2 parents e308d65 + 06b6788 commit 0a25330
Show file tree
Hide file tree
Showing 7 changed files with 980 additions and 0 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# Change Log
This project adheres to [Semantic Versioning](http://semver.org/). All notable changes will be documented in this file.

## [Unreleased](https://github.com/OldSneerJaw/borealis-pg-cli/compare/v1.2.0...HEAD)
- Adds the `borealis-pg:integrations` command to retrieve a list of data integrations for an add-on
- Adds the `borealis-pg:integrations:register` command to register a new data integration with an add-on
- Adds the `borealis-pg:integrations:remove` command to remove/deregister a data integration

## [1.2.0](https://github.com/OldSneerJaw/borealis-pg-cli/compare/v1.1.0...v1.2.0)
- Support the new secure tunnel connection info config var (`DATABASE_TUNNEL_BPG_CONN_INFO`)
- Require SSL/TLS for DB connections when using the `borealis-pg:run` command
Expand Down
138 changes: 138 additions & 0 deletions src/commands/borealis-pg/integrations/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import {borealisPgApiBaseUrl, expect, herokuApiBaseUrl, test} from '../../../test-utils'

const fakeAddonId = '0818035e-0103-4f85-880d-c3b4a712cf8d'
const fakeAddonName = 'borealis-pg-my-fake-addon'

const fakeAttachmentId = 'eaa7f0f9-9562-4ba3-b8dc-3c488ad73666'
const fakeAttachmentName = 'MY_COOL_DB'

const fakeHerokuAppId = '2ee2aea8-9a2f-48b2-8f86-b4aa504b35f7'
const fakeHerokuAppName = 'my-fake-heroku-app'

const fakeHerokuAuthToken = 'my-fake-heroku-auth-token'
const fakeHerokuAuthId = 'my-fake-heroku-auth'

const fakeIntegration1Name = 'my-first-fake-integration'
const fakeIntegration1DbUsername = 'my-first-fake-integration-db-user'
const fakeIntegration1SshUsername = 'my-first-fake-integration-ssh-user'
const fakeIntegration1WriteAccess = true
const fakeIntegration1CreatedAt = '2023-01-23T09:44:27.023Z'

const fakeIntegration2Name = 'my-second-fake-integration'
const fakeIntegration2DbUsername = 'my-second-fake-integration-db-user'
const fakeIntegration2SshUsername = 'my-second-fake-integration-ssh-user'
const fakeIntegration2WriteAccess = false
const fakeIntegration2CreatedAt = '2023-02-15T13:05:59.817Z'

const defaultTestContext = test.stdout()
.stderr()
.nock(herokuApiBaseUrl, api => api
.post('/oauth/authorizations', {
description: 'Borealis PG CLI plugin temporary auth token',
expires_in: 180,
scope: ['read', 'identity'],
})
.reply(201, {id: fakeHerokuAuthId, access_token: {token: fakeHerokuAuthToken}})
.delete(`/oauth/authorizations/${fakeHerokuAuthId}`)
.reply(200)
.get(`/apps/${fakeHerokuAppName}/addons`)
.reply(200, [
{
addon_service: {name: 'other-addon-service'},
id: '362885fa-b06b-434d-b3eb-a0ac53e3f840',
name: 'other-addon',
},
{addon_service: {name: 'borealis-pg'}, id: fakeAddonId, name: fakeAddonName},
])
.get(`/addons/${fakeAddonId}/addon-attachments`)
.reply(200, [
{
addon: {id: fakeAddonId, name: fakeAddonName},
app: {id: fakeHerokuAppId, name: fakeHerokuAppName},
id: fakeAttachmentId,
name: fakeAttachmentName,
},
]))

describe('data integration list command', () => {
defaultTestContext
.nock(
borealisPgApiBaseUrl,
{reqheaders: {authorization: `Bearer ${fakeHerokuAuthToken}`}},
api => api.get(`/heroku/resources/${fakeAddonName}/data-integrations`)
.reply(200, {
integrations: [
{
name: fakeIntegration1Name,
dbUsername: fakeIntegration1DbUsername,
sshUsername: fakeIntegration1SshUsername,
writeAccess: fakeIntegration1WriteAccess,
createdAt: fakeIntegration1CreatedAt,
},
{
name: fakeIntegration2Name,
dbUsername: fakeIntegration2DbUsername,
sshUsername: fakeIntegration2SshUsername,
writeAccess: fakeIntegration2WriteAccess,
createdAt: fakeIntegration2CreatedAt,
},
],
}))
.command(['borealis-pg:integrations', '--app', fakeHerokuAppName])
.it('outputs the list of registered data integrations', ctx => {
expect(ctx.stderr).to.endWith(
`Fetching data integration list for add-on ${fakeAddonName}... done\n`)

expect(ctx.stdout).to.containIgnoreSpaces(
' Data Integration DB Username SSH Username Write Access Created At')
expect(ctx.stdout).to.containIgnoreSpaces(
` ${fakeIntegration1Name} ${fakeIntegration1DbUsername} ${fakeIntegration1SshUsername} ${fakeIntegration1WriteAccess} ${fakeIntegration1CreatedAt} \n` +
` ${fakeIntegration2Name} ${fakeIntegration2DbUsername} ${fakeIntegration2SshUsername} ${fakeIntegration2WriteAccess} ${fakeIntegration2CreatedAt}`)
})

defaultTestContext
.nock(
borealisPgApiBaseUrl,
api => api.get(`/heroku/resources/${fakeAddonName}/data-integrations`)
.reply(200, {integrations: []}))
.command(['borealis-pg:integrations', '-a', fakeHerokuAppName])
.it('outputs a warning if there are no data integrations', ctx => {
expect(ctx.stderr).to.endWith(
`Fetching data integration list for add-on ${fakeAddonName}... done\n` +
' › Warning: No data integrations found\n')
expect(ctx.stdout).to.equal('')
})

defaultTestContext
.nock(
borealisPgApiBaseUrl,
api => api.get(`/heroku/resources/${fakeAddonName}/data-integrations`)
.reply(404, {reason: 'Does not exist'}))
.command(['borealis-pg:integrations', '-a', fakeHerokuAppName])
.catch('Add-on is not a Borealis Isolated Postgres add-on')
.it('exits with an error if the add-on was not found', ctx => {
expect(ctx.stdout).to.equal('')
})

defaultTestContext
.nock(
borealisPgApiBaseUrl,
api => api.get(`/heroku/resources/${fakeAddonName}/data-integrations`)
.reply(422, {reason: 'Not ready yet'}))
.command(['borealis-pg:integrations', '-a', fakeHerokuAppName])
.catch('Add-on is not finished provisioning')
.it('exits with an error if the add-on is not done provisioning', ctx => {
expect(ctx.stdout).to.equal('')
})

defaultTestContext
.nock(
borealisPgApiBaseUrl,
api => api.get(`/heroku/resources/${fakeAddonName}/data-integrations`)
.reply(500, {reason: 'Something went wrong'}))
.command(['borealis-pg:integrations', '-a', fakeHerokuAppName])
.catch('Add-on service is temporarily unavailable. Try again later.')
.it('exits with an error if the Borealis PG API indicates a server error', ctx => {
expect(ctx.stdout).to.equal('')
})
})
91 changes: 91 additions & 0 deletions src/commands/borealis-pg/integrations/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import color from '@heroku-cli/color'
import {Command} from '@heroku-cli/command'
import cli from 'cli-ux'
import {HTTP, HTTPError} from 'http-call'
import {applyActionSpinner} from '../../../async-actions'
import {getBorealisPgApiUrl, getBorealisPgAuthHeader} from '../../../borealis-api'
import {
addonOptionName,
appOptionName,
cliOptions,
processAddonAttachmentInfo,
} from '../../../command-components'
import {createHerokuAuth, fetchAddonAttachmentInfo, removeHerokuAuth} from '../../../heroku-api'

export default class ListDataIntegrationsCommand extends Command {
static description = `lists registered data integrations for a Borealis Isolated Postgres add-on
A data integration allows a third party service access to an add-on database
via a secure tunnel using semi-permanent SSH server and database credentials.`

static flags = {
[addonOptionName]: cliOptions.addon,
[appOptionName]: cliOptions.app,
}

async run() {
const {flags} = this.parse(ListDataIntegrationsCommand)
const authorization = await createHerokuAuth(this.heroku)
const attachmentInfo =
await fetchAddonAttachmentInfo(this.heroku, flags.addon, flags.app, this.error)
const {addonName} = processAddonAttachmentInfo(attachmentInfo, this.error)
try {
const response = await applyActionSpinner(
`Fetching data integration list for add-on ${color.addon(addonName)}`,
HTTP.get(
getBorealisPgApiUrl(`/heroku/resources/${addonName}/data-integrations`),
{headers: {Authorization: getBorealisPgAuthHeader(authorization)}}),
)

const responseBody = response.body as {integrations: Array<DataIntegrationInfo>}
if (responseBody.integrations.length > 0) {
const columns: {[name: string]: any} = {
name: {header: 'Data Integration'},
dbUsername: {header: 'DB Username'},
sshUsername: {header: 'SSH Username'},
writeAccess: {header: 'Write Access'},
createdAt: {header: 'Created At'},
}
const normalizedIntegrations = responseBody.integrations.map(value => {
return {
name: value.name,
dbUsername: value.dbUsername,
sshUsername: value.sshUsername,
writeAccess: value.writeAccess,
createdAt: new Date(value.createdAt),
}
})

this.log()
cli.table(normalizedIntegrations, columns, {'no-truncate': true})
} else {
this.warn('No data integrations found')
}
} finally {
await removeHerokuAuth(this.heroku, authorization.id as string)
}
}

async catch(err: any) {
/* istanbul ignore else */
if (err instanceof HTTPError) {
if (err.statusCode === 404) {
this.error('Add-on is not a Borealis Isolated Postgres add-on')
} else if (err.statusCode === 422) {
this.error('Add-on is not finished provisioning')
} else {
this.error('Add-on service is temporarily unavailable. Try again later.')
}
} else {
throw err
}
}
}

interface DataIntegrationInfo {
name: string;
dbUsername: string;
sshUsername: string;
writeAccess: boolean;
createdAt: string;
}
Loading

0 comments on commit 0a25330

Please sign in to comment.