-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #105 from OldSneerJaw/data-integrations
Support for data integrations
- Loading branch information
Showing
7 changed files
with
980 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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('') | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
Oops, something went wrong.