Skip to content

Commit

Permalink
Merge pull request aws#6153 from Bit-Quill/muaydin/docdb-polling-bug-fix
Browse files Browse the repository at this point in the history
feat(toolkit): Add DocumentDB support
  • Loading branch information
jpinkney-aws authored Feb 5, 2025
2 parents 50c9121 + 874eb41 commit 0cc411a
Show file tree
Hide file tree
Showing 55 changed files with 8,116 additions and 1 deletion.
2,649 changes: 2,649 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,8 @@
"@aws-sdk/client-cloudformation": "^3.667.0",
"@aws-sdk/client-cloudwatch-logs": "^3.666.0",
"@aws-sdk/client-cognito-identity": "^3.637.0",
"@aws-sdk/client-docdb": "^3.699.0",
"@aws-sdk/client-docdb-elastic": "^3.699.0",
"@aws-sdk/client-lambda": "^3.637.0",
"@aws-sdk/client-sso": "^3.342.0",
"@aws-sdk/client-sso-oidc": "^3.574.0",
Expand Down
12 changes: 12 additions & 0 deletions packages/core/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,17 @@
"AWS.command.apprunner.copyServiceUrl": "Copy Service URL",
"AWS.command.apprunner.open": "Open in Browser",
"AWS.command.apprunner.startDeployment": "Start Deployment",
"AWS.command.docdb.createCluster": "Create DocumentDB Cluster",
"AWS.command.docdb.createInstance": "Add Instance...",
"AWS.command.docdb.modifyInstance": "Modify Class...",
"AWS.command.docdb.rebootInstance": "Reboot Instance",
"AWS.command.docdb.startCluster": "Start Cluster",
"AWS.command.docdb.stopCluster": "Stop Cluster",
"AWS.command.docdb.tags": "Tags...",
"AWS.command.docdb.open": "Open in Browser",
"AWS.command.docdb.openHelp": "Open Getting Started Guide",
"AWS.command.docdb.copyEndpoint": "Copy Endpoint",
"AWS.command.docdb.addRegion": "Add region...",
"AWS.command.resources.copyIdentifier": "Copy Identifier",
"AWS.command.resources.configure": "Show Resources...",
"AWS.command.codewhisperer.introduction": "What is Amazon Q?",
Expand Down Expand Up @@ -272,6 +283,7 @@
"AWS.generic.copyUrl": "Copy URL",
"AWS.generic.promptDelete": "Delete...",
"AWS.generic.promptUpdate": "Update...",
"AWS.generic.promptRename": "Rename...",
"AWS.generic.preview": "Preview",
"AWS.generic.viewDocs": "View Documentation",
"AWS.generic.moreActions": "More Actions...",
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/awsexplorer/regionNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import { StepFunctionsNode } from '../stepFunctions/explorer/stepFunctionsNodes'
import { SsmDocumentNode } from '../ssmDocument/explorer/ssmDocumentNode'
import { ResourcesNode } from '../dynamicResources/explorer/nodes/resourcesNode'
import { AppRunnerNode } from '../awsService/apprunner/explorer/apprunnerNode'
import { DocumentDBNode } from '../docdb/explorer/docdbNode'
import { DefaultDocumentDBClient } from '../shared/clients/docdbClient'
import { DefaultAppRunnerClient } from '../shared/clients/apprunnerClient'
import { DefaultEcrClient } from '../shared/clients/ecrClient'
import { DefaultRedshiftClient } from '../shared/clients/redshiftClient'
Expand Down Expand Up @@ -57,6 +59,10 @@ const serviceCandidates: ServiceNode[] = [
serviceId: 'cloudformation',
createFn: (regionCode: string) => new CloudFormationNode(regionCode),
},
{
serviceId: 'docdb',
createFn: (regionCode: string) => new DocumentDBNode(DefaultDocumentDBClient.create(regionCode)),
},
{
serviceId: 'logs',
createFn: (regionCode: string) => new CloudWatchLogsNode(regionCode),
Expand Down
125 changes: 125 additions & 0 deletions packages/core/src/docdb/activation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*!
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

import { Commands } from '../shared'
import { ExtContext } from '../shared/extensions'
import { DBResourceNode } from './explorer/dbResourceNode'
import { DocumentDBNode } from './explorer/docdbNode'
import { DBClusterNode } from './explorer/dbClusterNode'
import { DBInstanceNode } from './explorer/dbInstanceNode'
import { addRegion } from './commands/addRegion'
import { createCluster } from './commands/createCluster'
import { deleteCluster } from './commands/deleteCluster'
import { renameCluster } from './commands/renameCluster'
import { startCluster } from './commands/startCluster'
import { stopCluster } from './commands/stopCluster'
import { createInstance } from './commands/createInstance'
import { deleteInstance } from './commands/deleteInstance'
import { modifyInstance } from './commands/modifyInstance'
import { rebootInstance } from './commands/rebootInstance'
import { renameInstance } from './commands/renameInstance'
import { addTag, listTags, removeTag } from './commands/tagCommands'
import { Uri } from 'vscode'
import { openUrl } from '../shared/utilities/vsCodeUtils'
import { getLogger } from '../shared/logger'

/**
* A utility function to automatically invoke trackChanges after a command.
*/

function withTrackChanges<T extends DBResourceNode>(
command: (node: T) => Promise<void>,
commandName: string = 'UnnamedCommand'
): (node: T) => Promise<void> {
return async (node: T) => {
const arn = node.arn || 'UnknownARN'
const startTime = new Date().toISOString()

getLogger().info(
`[${startTime}] Executing command "${commandName}" for resource with ARN: ${arn}. Tracking changes will be invoked post-execution.`
)

await command(node)

const endTime = new Date().toISOString()
getLogger().info(
`[${endTime}] Successfully executed command "${commandName}" for resource with ARN: ${arn}. Invoking trackChanges now.`
)

await node.trackChangesWithWaitProcessingStatus()
}
}

/**
* Activates DocumentDB components.
*/
export async function activate(ctx: ExtContext): Promise<void> {
ctx.extensionContext.subscriptions.push(
Commands.register('aws.docdb.createCluster', async (node?: DocumentDBNode) => {
await createCluster(node)
}),

Commands.register('aws.docdb.deleteCluster', withTrackChanges<DBClusterNode>(deleteCluster, 'deleteCluster')),

Commands.register('aws.docdb.renameCluster', withTrackChanges<DBClusterNode>(renameCluster, 'renameCluster')),

Commands.register('aws.docdb.startCluster', withTrackChanges<DBClusterNode>(startCluster, 'startCluster')),

Commands.register('aws.docdb.stopCluster', withTrackChanges<DBClusterNode>(stopCluster, 'stopCluster')),

Commands.register('aws.docdb.addRegion', withTrackChanges<DBClusterNode>(addRegion, 'addRegion')),

Commands.register(
'aws.docdb.createInstance',
withTrackChanges<DBClusterNode>(createInstance, 'createInstance')
),

Commands.register(
'aws.docdb.deleteInstance',
withTrackChanges<DBInstanceNode>(deleteInstance, 'deleteInstance')
),

Commands.register(
'aws.docdb.modifyInstance',
withTrackChanges<DBInstanceNode>(modifyInstance, 'modifyInstance')
),

Commands.register(
'aws.docdb.rebootInstance',
withTrackChanges<DBInstanceNode>(rebootInstance, 'rebootInstance')
),

Commands.register(
'aws.docdb.renameInstance',
withTrackChanges<DBInstanceNode>(renameInstance, 'renameInstance')
),

Commands.register('aws.docdb.listTags', async (node: DBResourceNode) => {
await listTags(node)
}),

Commands.register('aws.docdb.addTag', async (node: DBResourceNode) => {
await addTag(node)
}),

Commands.register('aws.docdb.removeTag', async (node: DBResourceNode) => {
await removeTag(node)
}),

Commands.register('aws.docdb.viewConsole', async (node?: DBResourceNode) => {
await node?.openInBrowser()
}),

Commands.register('aws.docdb.viewDocs', async () => {
await openUrl(
Uri.parse('https://docs.aws.amazon.com/documentdb/latest/developerguide/get-started-guide.html')
)
}),

Commands.register('aws.docdb.copyEndpoint', async (node?: DBResourceNode) => {
await node?.copyEndpoint()
})
)
}
165 changes: 165 additions & 0 deletions packages/core/src/docdb/commands/addRegion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/*!
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

import * as vscode from 'vscode'
import { getLogger } from '../../shared/logger'
import { telemetry } from '../../shared/telemetry'
import { localize } from '../../shared/utilities/vsCodeUtils'
import { DBClusterNode } from '../explorer/dbClusterNode'
import { DBGlobalClusterNode } from '../explorer/dbGlobalClusterNode'
import { DefaultDocumentDBClient } from '../../shared/clients/docdbClient'
import { ToolkitError } from '../../shared'
import { showViewLogsMessage } from '../../shared/utilities/messages'
import { isValidResponse } from '../../shared/wizards/wizard'
import { CancellationError } from '../../shared/utilities/timeoutUtils'
import { CreateGlobalClusterWizard } from '../wizards/createGlobalClusterWizard'
import { CreateDBClusterMessage } from '@aws-sdk/client-docdb'
import { createInstancesForCluster } from './createCluster'
import { isSupportedGlobalInstanceClass } from '../utils'

export async function addRegion(node: DBClusterNode | DBGlobalClusterNode): Promise<void> {
if (!node) {
throw new ToolkitError('No node specified for AddRegion')
}

return telemetry.docdb_addRegion.run(async () => {
let globalClusterName = undefined

if (node.cluster.StorageEncrypted) {
void vscode.window.showErrorMessage('Encrypted clusters are not supported')
return
}

if (node instanceof DBClusterNode) {
if (node.clusterRole !== 'regional') {
void vscode.window.showErrorMessage('Only regional clusters are supported')
return
}

if (node.cluster.DBClusterMembers?.length === 0) {
void vscode.window.showErrorMessage(
localize(
'AWS.docdb.addRegion.noInstances',
'Cluster must have at least one instance to add a region'
)
)
throw new ToolkitError('Cluster must have at least one instance to add a region', { cancelled: true })
}

const unsupportedInstanceFound = node.instances.find(
(instance) => !isSupportedGlobalInstanceClass(instance.DBInstanceClass!)
)

if (unsupportedInstanceFound) {
void vscode.window.showErrorMessage(
localize(
'AWS.docdb.addRegion.unsupportedInstanceClass',
'Instance class {0} not supported for global cluster. Upgrade the instances then try again.',
unsupportedInstanceFound.DBInstanceClass
)
)
throw new ToolkitError('Instance class not supported for global cluster', {
cancelled: true,
code: 'docdbInstanceClassNotSupported',
})
}
} else {
globalClusterName = node.cluster.GlobalClusterIdentifier

if (node.cluster.GlobalClusterMembers!.length > 4) {
void vscode.window.showErrorMessage(
localize('AWS.docdb.addRegion.maxRegions', 'Global clusters can have a maximum of 5 regions')
)
throw new ToolkitError('Global clusters can have a maximum of 5 regions', {
cancelled: true,
code: 'docdbMaxRegionsInUse',
})
}
}

if (!node.isAvailable) {
void vscode.window.showErrorMessage(localize('AWS.docdb.clusterStopped', 'Cluster must be running'))
throw new ToolkitError('Cluster not available', { cancelled: true, code: 'docdbClusterStopped' })
}

const wizard = new CreateGlobalClusterWizard(node.regionCode, node.cluster.EngineVersion, node.client, {
initState: { GlobalClusterName: globalClusterName },
})
const response = await wizard.run()

if (!isValidResponse(response)) {
throw new CancellationError('user')
}

const regionCode = response.RegionCode
let input: CreateDBClusterMessage
let clusterName = response.GlobalClusterName

try {
if (node instanceof DBClusterNode) {
// Create new global cluster from regional cluster
const primaryCluster = node.cluster

getLogger().info(`docdb: Creating global cluster: ${clusterName}`)
const globalCluster = await node.client.createGlobalCluster({
GlobalClusterIdentifier: response.GlobalClusterName,
SourceDBClusterIdentifier: primaryCluster.DBClusterArn,
})

input = {
GlobalClusterIdentifier: globalCluster?.GlobalClusterIdentifier,
DBClusterIdentifier: response.Cluster.DBClusterIdentifier,
DeletionProtection: primaryCluster.DeletionProtection,
Engine: primaryCluster.Engine,
EngineVersion: primaryCluster.EngineVersion,
StorageType: primaryCluster.StorageType,
StorageEncrypted: globalCluster?.StorageEncrypted,
}
} else {
// Add secondary cluster to global cluster
const globalCluster = node.cluster

input = {
GlobalClusterIdentifier: globalClusterName,
DBClusterIdentifier: response.Cluster.DBClusterIdentifier,
DeletionProtection: globalCluster.DeletionProtection,
Engine: globalCluster.Engine,
EngineVersion: globalCluster.EngineVersion,
StorageEncrypted: globalCluster.StorageEncrypted,
}
}

clusterName = response.Cluster.DBClusterIdentifier
getLogger().info(`docdb: Creating secondary cluster: ${clusterName} in region ${regionCode}`)

const client = DefaultDocumentDBClient.create(regionCode)
const newCluster = await client.createCluster(input)

if (response.Cluster.DBInstanceCount) {
await createInstancesForCluster(
client,
clusterName,
response.Cluster.DBInstanceClass,
response.Cluster.DBInstanceCount
)
}

getLogger().info('docdb: Created cluster: %O', newCluster)
void vscode.window.showInformationMessage(localize('AWS.docdb.addRegion.success', 'Region added'))

if (node instanceof DBClusterNode) {
node?.parent.refresh()
} else {
node?.refresh()
}
} catch (e) {
getLogger().error(`docdb: Failed to create cluster ${clusterName}: %s`, e)
void showViewLogsMessage(
localize('AWS.docdb.createCluster.error', 'Failed to create cluster: {0}', clusterName)
)
throw ToolkitError.chain(e, `Failed to create cluster ${clusterName}`)
}
})
}
Loading

0 comments on commit 0cc411a

Please sign in to comment.