diff --git a/packages/core/package.nls.json b/packages/core/package.nls.json index 94dcd9c8832..d83d4b1ff02 100644 --- a/packages/core/package.nls.json +++ b/packages/core/package.nls.json @@ -129,6 +129,7 @@ "AWS.command.ec2.stopInstance": "Stop EC2 Instance", "AWS.command.ec2.rebootInstance": "Reboot EC2 Instance", "AWS.command.ec2.copyInstanceId": "Copy Instance Id", + "AWS.command.ec2.viewLogs": "View EC2 Logs", "AWS.command.ecr.copyTagUri": "Copy Tag URI", "AWS.command.ecr.copyRepositoryUri": "Copy Repository URI", "AWS.command.ecr.createRepository": "Create Repository...", diff --git a/packages/core/src/awsService/cloudWatchLogs/changeLogSearch.ts b/packages/core/src/awsService/cloudWatchLogs/changeLogSearch.ts index 4797e06fc03..349f7339e87 100644 --- a/packages/core/src/awsService/cloudWatchLogs/changeLogSearch.ts +++ b/packages/core/src/awsService/cloudWatchLogs/changeLogSearch.ts @@ -5,7 +5,7 @@ import { CancellationError } from '../../shared/utilities/timeoutUtils' import { telemetry } from '../../shared/telemetry/telemetry' import { showInputBox } from '../../shared/ui/inputPrompter' -import { createURIFromArgs, isLogStreamUri, recordTelemetryFilter } from './cloudWatchLogsUtils' +import { cwlUriSchema, isLogStreamUri, recordTelemetryFilter } from './cloudWatchLogsUtils' import { prepareDocument } from './commands/searchLogGroup' import { getActiveDocumentUri } from './document/logDataDocumentProvider' import { CloudWatchLogsData, filterLogEventsFromUri, LogDataRegistry } from './registry/logDataRegistry' @@ -98,7 +98,7 @@ export async function changeLogSearchParams( throw new CancellationError('user') } - const newUri = createURIFromArgs(newData.logGroupInfo, newData.parameters) + const newUri = cwlUriSchema.form({ logGroupInfo: newData.logGroupInfo, parameters: newData.parameters }) await prepareDocument(newUri, newData, registry) }) } diff --git a/packages/core/src/awsService/cloudWatchLogs/cloudWatchLogsUtils.ts b/packages/core/src/awsService/cloudWatchLogs/cloudWatchLogsUtils.ts index b8c769e92f8..e2356595153 100644 --- a/packages/core/src/awsService/cloudWatchLogs/cloudWatchLogsUtils.ts +++ b/packages/core/src/awsService/cloudWatchLogs/cloudWatchLogsUtils.ts @@ -6,8 +6,9 @@ import { telemetry } from '../../shared/telemetry/telemetry' import * as vscode from 'vscode' import { CLOUDWATCH_LOGS_SCHEME } from '../../shared/constants' import { fromExtensionManifest } from '../../shared/settings' -import { CloudWatchLogsData, CloudWatchLogsGroupInfo } from './registry/logDataRegistry' +import { CloudWatchLogsArgs, CloudWatchLogsData, CloudWatchLogsGroupInfo } from './registry/logDataRegistry' import { CloudWatchLogsParameters } from './registry/logDataRegistry' +import { UriSchema } from '../../shared/utilities/uriUtils' // URIs are the only vehicle for delivering information to a TextDocumentContentProvider. // The following functions are used to structure and destructure relevant information to/from a URI. @@ -32,8 +33,7 @@ export function recordTelemetryFilter(logData: CloudWatchLogsData): void { export function uriToKey(uri: vscode.Uri): string { if (uri.query) { try { - const { filterPattern, startTime, endTime, limit, streamNameOptions } = - parseCloudWatchLogsUri(uri).parameters + const { filterPattern, startTime, endTime, limit, streamNameOptions } = cwlUriSchema.parse(uri).parameters const parts = [uri.path, filterPattern, startTime, endTime, limit, streamNameOptions] return parts.map((p) => p ?? '').join(':') } catch { @@ -52,7 +52,7 @@ export function uriToKey(uri: vscode.Uri): string { * message as the actual log group search. That results in a more fluid UX. */ export function msgKey(logGroupInfo: CloudWatchLogsGroupInfo): string { - const uri = createURIFromArgs(logGroupInfo, {}) + const uri = cwlUriSchema.form({ logGroupInfo: logGroupInfo, parameters: {} }) return uri.toString() } @@ -60,10 +60,7 @@ export function msgKey(logGroupInfo: CloudWatchLogsGroupInfo): string { * Destructures an awsCloudWatchLogs URI into its component pieces. * @param uri URI for a Cloudwatch Logs file */ -export function parseCloudWatchLogsUri(uri: vscode.Uri): { - logGroupInfo: CloudWatchLogsGroupInfo - parameters: CloudWatchLogsParameters -} { +function parseCloudWatchLogsUri(uri: vscode.Uri): CloudWatchLogsArgs { const parts = uri.path.split(':') if (uri.scheme !== CLOUDWATCH_LOGS_SCHEME) { @@ -85,18 +82,8 @@ export function parseCloudWatchLogsUri(uri: vscode.Uri): { } } -/** True if given URI is a valid Cloud Watch Logs Uri */ -export function isCwlUri(uri: vscode.Uri): boolean { - try { - parseCloudWatchLogsUri(uri) - return true - } catch { - return false - } -} - export function patternFromCwlUri(uri: vscode.Uri): CloudWatchLogsParameters['filterPattern'] { - return parseCloudWatchLogsUri(uri).parameters.filterPattern + return cwlUriSchema.parse(uri).parameters.filterPattern } /** @@ -105,7 +92,7 @@ export function patternFromCwlUri(uri: vscode.Uri): CloudWatchLogsParameters['fi * @returns */ export function isLogStreamUri(uri: vscode.Uri): boolean { - const logGroupInfo = parseCloudWatchLogsUri(uri).logGroupInfo + const logGroupInfo = cwlUriSchema.parse(uri).logGroupInfo return logGroupInfo.streamName !== undefined } @@ -115,15 +102,13 @@ export function isLogStreamUri(uri: vscode.Uri): boolean { * @param streamName Log stream name * @param regionName AWS region */ -export function createURIFromArgs( - logGroupInfo: CloudWatchLogsGroupInfo, - parameters: CloudWatchLogsParameters -): vscode.Uri { - let uriStr = `${CLOUDWATCH_LOGS_SCHEME}:${logGroupInfo.regionName}:${logGroupInfo.groupName}` - uriStr += logGroupInfo.streamName ? `:${logGroupInfo.streamName}` : '' +function createURIFromArgs(args: CloudWatchLogsArgs): vscode.Uri { + let uriStr = `${CLOUDWATCH_LOGS_SCHEME}:${args.logGroupInfo.regionName}:${args.logGroupInfo.groupName}` + uriStr += args.logGroupInfo.streamName ? `:${args.logGroupInfo.streamName}` : '' - uriStr += `?${encodeURIComponent(JSON.stringify(parameters))}` + uriStr += `?${encodeURIComponent(JSON.stringify(args.parameters))}` return vscode.Uri.parse(uriStr) } +export const cwlUriSchema = new UriSchema(parseCloudWatchLogsUri, createURIFromArgs) export class CloudWatchLogsSettings extends fromExtensionManifest('aws.cwl', { limit: Number }) {} diff --git a/packages/core/src/awsService/cloudWatchLogs/commands/copyLogResource.ts b/packages/core/src/awsService/cloudWatchLogs/commands/copyLogResource.ts index b7cca40c752..846f87e797e 100644 --- a/packages/core/src/awsService/cloudWatchLogs/commands/copyLogResource.ts +++ b/packages/core/src/awsService/cloudWatchLogs/commands/copyLogResource.ts @@ -7,7 +7,7 @@ import * as nls from 'vscode-nls' const localize = nls.loadMessageBundle() import * as vscode from 'vscode' -import { isLogStreamUri, parseCloudWatchLogsUri } from '../cloudWatchLogsUtils' +import { cwlUriSchema, isLogStreamUri } from '../cloudWatchLogsUtils' import { copyToClipboard } from '../../../shared/utilities/messages' export async function copyLogResource(uri?: vscode.Uri): Promise { @@ -20,7 +20,7 @@ export async function copyLogResource(uri?: vscode.Uri): Promise { throw new Error('no active text editor, or undefined URI') } } - const parsedUri = parseCloudWatchLogsUri(uri) + const parsedUri = cwlUriSchema.parse(uri) const resourceName = isLogStreamUri(uri) ? parsedUri.logGroupInfo.streamName : parsedUri.logGroupInfo.groupName if (!resourceName) { diff --git a/packages/core/src/awsService/cloudWatchLogs/commands/saveCurrentLogDataContent.ts b/packages/core/src/awsService/cloudWatchLogs/commands/saveCurrentLogDataContent.ts index 2e83e04cf76..9d1600fafba 100644 --- a/packages/core/src/awsService/cloudWatchLogs/commands/saveCurrentLogDataContent.ts +++ b/packages/core/src/awsService/cloudWatchLogs/commands/saveCurrentLogDataContent.ts @@ -7,7 +7,7 @@ import * as vscode from 'vscode' import * as nls from 'vscode-nls' const localize = nls.loadMessageBundle() -import { isLogStreamUri, parseCloudWatchLogsUri } from '../cloudWatchLogsUtils' +import { cwlUriSchema, isLogStreamUri } from '../cloudWatchLogsUtils' import { telemetry, CloudWatchResourceType, Result } from '../../../shared/telemetry/telemetry' import fs from '../../../shared/fs/fs' @@ -28,7 +28,7 @@ export async function saveCurrentLogDataContent(): Promise { const workspaceDir = vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders[0].uri : vscode.Uri.file(fs.getUserHomeDir()) - const uriComponents = parseCloudWatchLogsUri(uri) + const uriComponents = cwlUriSchema.parse(uri) const logGroupInfo = uriComponents.logGroupInfo const localizedLogFile = localize('AWS.command.saveCurrentLogDataContent.logfile', 'Log File') diff --git a/packages/core/src/awsService/cloudWatchLogs/commands/searchLogGroup.ts b/packages/core/src/awsService/cloudWatchLogs/commands/searchLogGroup.ts index 0dfc491bbb4..41ce8d7922f 100644 --- a/packages/core/src/awsService/cloudWatchLogs/commands/searchLogGroup.ts +++ b/packages/core/src/awsService/cloudWatchLogs/commands/searchLogGroup.ts @@ -16,7 +16,7 @@ import { } from '../registry/logDataRegistry' import { DataQuickPickItem } from '../../../shared/ui/pickerPrompter' import { isValidResponse, isWizardControl, Wizard, WIZARD_RETRY } from '../../../shared/wizards/wizard' -import { createURIFromArgs, msgKey, parseCloudWatchLogsUri, recordTelemetryFilter } from '../cloudWatchLogsUtils' +import { cwlUriSchema, msgKey, recordTelemetryFilter } from '../cloudWatchLogsUtils' import { DefaultCloudWatchLogsClient } from '../../../shared/clients/cloudWatchLogsClient' import { CancellationError } from '../../../shared/utilities/timeoutUtils' import { getLogger } from '../../../shared/logger' @@ -29,6 +29,7 @@ import { createBackButton, createExitButton, createHelpButton } from '../../../s import { PromptResult } from '../../../shared/ui/prompter' import { ToolkitError } from '../../../shared/errors' import { Messages } from '../../../shared/utilities/messages' +import { showFile } from '../../../shared/utilities/textDocumentUtilities' const localize = nls.loadMessageBundle() @@ -65,9 +66,7 @@ export async function prepareDocument(uri: vscode.Uri, logData: CloudWatchLogsDa try { // Gets the data: calls filterLogEventsFromUri(). await registry.fetchNextLogEvents(uri) - const doc = await vscode.workspace.openTextDocument(uri) - await vscode.window.showTextDocument(doc, { preview: false }) - await vscode.languages.setTextDocumentLanguage(doc, 'log') + await showFile(uri) } catch (err) { if (CancellationError.isUserCancelled(err)) { throw err @@ -78,7 +77,7 @@ export async function prepareDocument(uri: vscode.Uri, logData: CloudWatchLogsDa localize( 'AWS.cwl.searchLogGroup.errorRetrievingLogs', 'Failed to get logs for {0}', - parseCloudWatchLogsUri(uri).logGroupInfo.groupName + cwlUriSchema.parse(uri).logGroupInfo.groupName ) ) } @@ -105,7 +104,7 @@ export async function searchLogGroup( } const userResponse = handleWizardResponse(response, registry) - const uri = createURIFromArgs(userResponse.logGroupInfo, userResponse.parameters) + const uri = cwlUriSchema.form({ logGroupInfo: userResponse.logGroupInfo, parameters: userResponse.parameters }) await prepareDocument(uri, userResponse, registry) }) } diff --git a/packages/core/src/awsService/cloudWatchLogs/commands/viewLogStream.ts b/packages/core/src/awsService/cloudWatchLogs/commands/viewLogStream.ts index a8ba6dbba76..2bfafd6d2d7 100644 --- a/packages/core/src/awsService/cloudWatchLogs/commands/viewLogStream.ts +++ b/packages/core/src/awsService/cloudWatchLogs/commands/viewLogStream.ts @@ -21,11 +21,11 @@ import { initLogData as initLogData, filterLogEventsFromUri, } from '../registry/logDataRegistry' -import { createURIFromArgs } from '../cloudWatchLogsUtils' import { prepareDocument, searchLogGroup } from './searchLogGroup' import { telemetry, Result } from '../../../shared/telemetry/telemetry' import { CancellationError } from '../../../shared/utilities/timeoutUtils' import { formatLocalized } from '../../../shared/utilities/textUtilities' +import { cwlUriSchema } from '../cloudWatchLogsUtils' export async function viewLogStream(node: LogGroupNode, registry: LogDataRegistry): Promise { await telemetry.cloudwatchlogs_open.run(async (span) => { @@ -52,7 +52,7 @@ export async function viewLogStream(node: LogGroupNode, registry: LogDataRegistr limit: registry.configuration.get('limit', 10000), } - const uri = createURIFromArgs(logGroupInfo, parameters) + const uri = cwlUriSchema.form({ logGroupInfo: logGroupInfo, parameters: parameters }) const logData = initLogData(logGroupInfo, parameters, filterLogEventsFromUri) await prepareDocument(uri, logData, registry) }) diff --git a/packages/core/src/awsService/cloudWatchLogs/document/logDataDocumentProvider.ts b/packages/core/src/awsService/cloudWatchLogs/document/logDataDocumentProvider.ts index 8eb390f7760..5925911893b 100644 --- a/packages/core/src/awsService/cloudWatchLogs/document/logDataDocumentProvider.ts +++ b/packages/core/src/awsService/cloudWatchLogs/document/logDataDocumentProvider.ts @@ -5,8 +5,8 @@ import * as vscode from 'vscode' import { CloudWatchLogsGroupInfo, LogDataRegistry, UriString } from '../registry/logDataRegistry' import { getLogger } from '../../../shared/logger' -import { isCwlUri } from '../cloudWatchLogsUtils' import { generateTextFromLogEvents, LineToLogStreamMap } from './textContent' +import { cwlUriSchema } from '../cloudWatchLogsUtils' export class LogDataDocumentProvider implements vscode.TextDocumentContentProvider { /** Resolves the correct {@link LineToLogStreamMap} instance for a given URI */ @@ -26,7 +26,7 @@ export class LogDataDocumentProvider implements vscode.TextDocumentContentProvid } public provideTextDocumentContent(uri: vscode.Uri): string { - if (!isCwlUri(uri)) { + if (!cwlUriSchema.isValid(uri)) { throw new Error(`Uri is not a CWL Uri, so no text can be provided: ${uri.toString()}`) } const events = this.registry.fetchCachedLogEvents(uri) diff --git a/packages/core/src/awsService/cloudWatchLogs/document/logStreamsCodeLensProvider.ts b/packages/core/src/awsService/cloudWatchLogs/document/logStreamsCodeLensProvider.ts index 5c83ab929d9..9870ff009d2 100644 --- a/packages/core/src/awsService/cloudWatchLogs/document/logStreamsCodeLensProvider.ts +++ b/packages/core/src/awsService/cloudWatchLogs/document/logStreamsCodeLensProvider.ts @@ -6,12 +6,7 @@ import * as vscode from 'vscode' import { CLOUDWATCH_LOGS_SCHEME } from '../../../shared/constants' import { CloudWatchLogsGroupInfo, LogDataRegistry } from '../registry/logDataRegistry' -import { - CloudWatchLogsSettings, - createURIFromArgs, - isLogStreamUri, - parseCloudWatchLogsUri, -} from '../cloudWatchLogsUtils' +import { CloudWatchLogsSettings, cwlUriSchema, isLogStreamUri } from '../cloudWatchLogsUtils' import { LogDataDocumentProvider } from './logDataDocumentProvider' type IdWithLine = { streamId: string; lineNum: number } @@ -43,7 +38,7 @@ export class LogStreamCodeLensProvider implements vscode.CodeLensProvider { return [] } - const logGroupInfo = parseCloudWatchLogsUri(uri).logGroupInfo + const logGroupInfo = cwlUriSchema.parse(uri).logGroupInfo if (logGroupInfo.streamName) { // This means we have a stream file not a log search. @@ -64,7 +59,11 @@ export class LogStreamCodeLensProvider implements vscode.CodeLensProvider { createLogStreamCodeLens(logGroupInfo: CloudWatchLogsGroupInfo, idWithLine: IdWithLine): vscode.CodeLens { const settings = new CloudWatchLogsSettings() const limit = settings.get('limit', 1000) - const streamUri = createURIFromArgs({ ...logGroupInfo, streamName: idWithLine.streamId }, { limit: limit }) + const cwlArgs = { + logGroupInfo: { ...logGroupInfo, streamName: idWithLine.streamId }, + parameters: { limit: limit }, + } + const streamUri = cwlUriSchema.form(cwlArgs) const cmd: vscode.Command = { command: 'aws.loadLogStreamFile', arguments: [streamUri, this.registry], diff --git a/packages/core/src/awsService/cloudWatchLogs/registry/logDataRegistry.ts b/packages/core/src/awsService/cloudWatchLogs/registry/logDataRegistry.ts index e8ae51558e5..88945d61bd2 100644 --- a/packages/core/src/awsService/cloudWatchLogs/registry/logDataRegistry.ts +++ b/packages/core/src/awsService/cloudWatchLogs/registry/logDataRegistry.ts @@ -5,7 +5,7 @@ import * as vscode from 'vscode' import { CloudWatchLogs } from 'aws-sdk' -import { CloudWatchLogsSettings, parseCloudWatchLogsUri, uriToKey, msgKey } from '../cloudWatchLogsUtils' +import { CloudWatchLogsSettings, uriToKey, msgKey, cwlUriSchema } from '../cloudWatchLogsUtils' import { DefaultCloudWatchLogsClient } from '../../../shared/clients/cloudWatchLogsClient' import { waitTimeout } from '../../../shared/utilities/timeoutUtils' import { Messages } from '../../../shared/utilities/messages' @@ -190,7 +190,7 @@ export class LogDataRegistry { if (this.isRegistered(uri)) { throw new Error(`Already registered: ${uri.toString()}`) } - const data = parseCloudWatchLogsUri(uri) + const data = cwlUriSchema.parse(uri) this.setLogData(uri, initLogData(data.logGroupInfo, data.parameters, retrieveLogsFunction)) } @@ -281,6 +281,11 @@ export function initLogData( } } +export type CloudWatchLogsArgs = { + logGroupInfo: CloudWatchLogsGroupInfo + parameters: CloudWatchLogsParameters +} + export type CloudWatchLogsGroupInfo = { groupName: string regionName: string diff --git a/packages/core/src/awsService/ec2/activation.ts b/packages/core/src/awsService/ec2/activation.ts index 550a0bbf47c..45d0a5c369e 100644 --- a/packages/core/src/awsService/ec2/activation.ts +++ b/packages/core/src/awsService/ec2/activation.ts @@ -2,6 +2,7 @@ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ +import * as vscode from 'vscode' import { ExtContext } from '../../shared/extensions' import { Commands } from '../../shared/vscode/commands2' import { telemetry } from '../../shared/telemetry/telemetry' @@ -15,10 +16,16 @@ import { startInstance, stopInstance, refreshExplorer, + openLogDocument, linkToLaunchInstance, } from './commands' +import { ec2LogsScheme } from '../../shared/constants' +import { Ec2LogDocumentProvider } from './ec2LogDocumentProvider' export async function activate(ctx: ExtContext): Promise { + ctx.extensionContext.subscriptions.push( + vscode.workspace.registerTextDocumentContentProvider(ec2LogsScheme, new Ec2LogDocumentProvider()) + ) ctx.extensionContext.subscriptions.push( Commands.register('aws.ec2.openTerminal', async (node?: Ec2InstanceNode) => { await telemetry.ec2_connectToInstance.run(async (span) => { @@ -30,6 +37,9 @@ export async function activate(ctx: ExtContext): Promise { Commands.register('aws.ec2.copyInstanceId', async (node: Ec2InstanceNode) => { await copyTextCommand(node, 'id') }), + Commands.register('aws.ec2.viewLogs', async (node?: Ec2InstanceNode) => { + await openLogDocument(node) + }), Commands.register('aws.ec2.openRemoteConnection', async (node?: Ec2Node) => { await openRemoteConnection(node) diff --git a/packages/core/src/awsService/ec2/commands.ts b/packages/core/src/awsService/ec2/commands.ts index f09ea0ea10c..01cdb5b5f53 100644 --- a/packages/core/src/awsService/ec2/commands.ts +++ b/packages/core/src/awsService/ec2/commands.ts @@ -2,7 +2,6 @@ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ - import { Ec2InstanceNode } from './explorer/ec2InstanceNode' import { Ec2Node } from './explorer/ec2ParentNode' import { Ec2ConnectionManager } from './model' @@ -10,9 +9,11 @@ import { Ec2Prompter, instanceFilter, Ec2Selection } from './prompter' import { SafeEc2Instance, Ec2Client } from '../../shared/clients/ec2Client' import { copyToClipboard } from '../../shared/utilities/messages' import { getLogger } from '../../shared/logger' +import { ec2LogSchema } from './ec2LogDocumentProvider' import { getAwsConsoleUrl } from '../../shared/awsConsole' import { showRegionPrompter } from '../../auth/utils' import { openUrl } from '../../shared/utilities/vsCodeUtils' +import { showFile } from '../../shared/utilities/textDocumentUtilities' export function refreshExplorer(node?: Ec2Node) { if (node) { @@ -71,3 +72,7 @@ async function getSelection(node?: Ec2Node, filter?: instanceFilter): Promise { await copyToClipboard(instanceId, 'Id') } + +export async function openLogDocument(node?: Ec2InstanceNode): Promise { + return await showFile(ec2LogSchema.form(await getSelection(node))) +} diff --git a/packages/core/src/awsService/ec2/ec2LogDocumentProvider.ts b/packages/core/src/awsService/ec2/ec2LogDocumentProvider.ts new file mode 100644 index 00000000000..1c8e68f0e02 --- /dev/null +++ b/packages/core/src/awsService/ec2/ec2LogDocumentProvider.ts @@ -0,0 +1,42 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import * as vscode from 'vscode' +import { Ec2Selection } from './prompter' +import { Ec2Client } from '../../shared/clients/ec2Client' +import { ec2LogsScheme } from '../../shared/constants' +import { UriSchema } from '../../shared/utilities/uriUtils' + +export class Ec2LogDocumentProvider implements vscode.TextDocumentContentProvider { + public constructor() {} + + public async provideTextDocumentContent(uri: vscode.Uri): Promise { + if (!ec2LogSchema.isValid(uri)) { + throw new Error(`Invalid EC2 Logs URI: ${uri.toString()}`) + } + const ec2Selection = ec2LogSchema.parse(uri) + const ec2Client = new Ec2Client(ec2Selection.region) + const consoleOutput = await ec2Client.getConsoleOutput(ec2Selection.instanceId, false) + return consoleOutput.Output + } +} + +export const ec2LogSchema = new UriSchema(parseEc2Uri, formEc2Uri) + +function parseEc2Uri(uri: vscode.Uri): Ec2Selection { + const parts = uri.path.split(':') + + if (uri.scheme !== ec2LogsScheme) { + throw new Error(`URI ${uri} is not parseable for EC2 Logs`) + } + + return { + instanceId: parts[1], + region: parts[0], + } +} + +function formEc2Uri(selection: Ec2Selection): vscode.Uri { + return vscode.Uri.parse(`${ec2LogsScheme}:${selection.region}:${selection.instanceId}`) +} diff --git a/packages/core/src/lambda/vue/remoteInvoke/invokeLambda.ts b/packages/core/src/lambda/vue/remoteInvoke/invokeLambda.ts index a644b3b912f..b75dd9477ba 100644 --- a/packages/core/src/lambda/vue/remoteInvoke/invokeLambda.ts +++ b/packages/core/src/lambda/vue/remoteInvoke/invokeLambda.ts @@ -20,6 +20,7 @@ import * as nls from 'vscode-nls' import { VueWebview } from '../../../webviews/main' import { telemetry } from '../../../shared/telemetry/telemetry' import { Result } from '../../../shared/telemetry/telemetry' +import { decodeBase64 } from '../../../shared' const localize = nls.loadMessageBundle() @@ -61,7 +62,7 @@ export class RemoteInvokeWebview extends VueWebview { try { const funcResponse = await this.client.invoke(this.data.FunctionArn, input) - const logs = funcResponse.LogResult ? Buffer.from(funcResponse.LogResult, 'base64').toString() : '' + const logs = funcResponse.LogResult ? decodeBase64(funcResponse.LogResult) : '' const payload = funcResponse.Payload ? funcResponse.Payload : JSON.stringify({}) this.channel.appendLine(`Invocation result for ${this.data.FunctionArn}`) diff --git a/packages/core/src/shared/clients/ec2Client.ts b/packages/core/src/shared/clients/ec2Client.ts index 49ff4841e58..490da6dda3f 100644 --- a/packages/core/src/shared/clients/ec2Client.ts +++ b/packages/core/src/shared/clients/ec2Client.ts @@ -12,6 +12,7 @@ import { PromiseResult } from 'aws-sdk/lib/request' import { Timeout } from '../utilities/timeoutUtils' import { showMessageWithCancel } from '../utilities/messages' import { ToolkitError, isAwsError } from '../errors' +import { decodeBase64 } from '../utilities/textUtilities' /** * A wrapper around EC2.Instance where we can safely assume InstanceId field exists. @@ -22,6 +23,11 @@ export interface SafeEc2Instance extends EC2.Instance { LastSeenStatus: EC2.InstanceStateName } +interface SafeEc2GetConsoleOutputResult extends EC2.GetConsoleOutputRequest { + Output: string + InstanceId: string +} + export class Ec2Client { public constructor(public readonly regionCode: string) {} @@ -229,6 +235,16 @@ export class Ec2Client { const association = await this.getIamInstanceProfileAssociation(instanceId) return association ? association.IamInstanceProfile : undefined } + + public async getConsoleOutput(instanceId: string, latest: boolean): Promise { + const client = await this.createSdkClient() + const response = await client.getConsoleOutput({ InstanceId: instanceId, Latest: latest }).promise() + return { + ...response, + InstanceId: instanceId, + Output: response.Output ? decodeBase64(response.Output) : '', + } + } } export function getNameOfInstance(instance: EC2.Instance): string | undefined { diff --git a/packages/core/src/shared/constants.ts b/packages/core/src/shared/constants.ts index 908624383dc..2a7a67355b5 100644 --- a/packages/core/src/shared/constants.ts +++ b/packages/core/src/shared/constants.ts @@ -129,6 +129,7 @@ export const ecsIamPermissionsUrl = vscode.Uri.parse( */ export const CLOUDWATCH_LOGS_SCHEME = 'aws-cwl' // eslint-disable-line @typescript-eslint/naming-convention export const AWS_SCHEME = 'aws' // eslint-disable-line @typescript-eslint/naming-convention +export const ec2LogsScheme = 'aws-ec2' export const amazonQDiffScheme = 'amazon-q-diff' export const lambdaPackageTypeImage = 'Image' diff --git a/packages/core/src/shared/utilities/textDocumentUtilities.ts b/packages/core/src/shared/utilities/textDocumentUtilities.ts index 71114bd2389..5a47e231b80 100644 --- a/packages/core/src/shared/utilities/textDocumentUtilities.ts +++ b/packages/core/src/shared/utilities/textDocumentUtilities.ts @@ -184,3 +184,9 @@ export function getIndentedCode(message: any, doc: vscode.TextDocument, selectio return indent(message.code, indentation.length) } + +export async function showFile(uri: vscode.Uri) { + const doc = await vscode.workspace.openTextDocument(uri) + await vscode.window.showTextDocument(doc, { preview: false }) + await vscode.languages.setTextDocumentLanguage(doc, 'log') +} diff --git a/packages/core/src/shared/utilities/textUtilities.ts b/packages/core/src/shared/utilities/textUtilities.ts index f337bb31276..46423288645 100644 --- a/packages/core/src/shared/utilities/textUtilities.ts +++ b/packages/core/src/shared/utilities/textUtilities.ts @@ -406,6 +406,9 @@ export function undefinedIfEmpty(str: string | undefined): string | undefined { return undefined } +export function decodeBase64(base64Str: string): string { + return Buffer.from(base64Str, 'base64').toString() +} /** * Extracts the file path and selection context from the message. * diff --git a/packages/core/src/shared/utilities/uriUtils.ts b/packages/core/src/shared/utilities/uriUtils.ts index edf747a4df4..deca450570b 100644 --- a/packages/core/src/shared/utilities/uriUtils.ts +++ b/packages/core/src/shared/utilities/uriUtils.ts @@ -26,6 +26,27 @@ export function fromQueryToParameters(query: vscode.Uri['query']): Map { + public constructor( + public parse: (uri: vscode.Uri) => T, + public form: (obj: T) => vscode.Uri + ) {} + + public isValid(uri: vscode.Uri): boolean { + try { + this.parse(uri) + return true + } catch (e) { + return false + } + } +} + /** * Converts a string path to a Uri, or returns the given Uri if it is already a Uri. * diff --git a/packages/core/src/test/awsService/cloudWatchLogs/commands/copyLogResource.test.ts b/packages/core/src/test/awsService/cloudWatchLogs/commands/copyLogResource.test.ts index 1405fb7e619..3dc329c8c34 100644 --- a/packages/core/src/test/awsService/cloudWatchLogs/commands/copyLogResource.test.ts +++ b/packages/core/src/test/awsService/cloudWatchLogs/commands/copyLogResource.test.ts @@ -5,8 +5,8 @@ import assert from 'assert' import * as vscode from 'vscode' -import { createURIFromArgs } from '../../../../awsService/cloudWatchLogs/cloudWatchLogsUtils' import { copyLogResource } from '../../../../awsService/cloudWatchLogs/commands/copyLogResource' +import { cwlUriSchema } from '../../../../awsService/cloudWatchLogs/cloudWatchLogsUtils' describe('copyLogResource', async function () { beforeEach(async function () { @@ -23,7 +23,7 @@ describe('copyLogResource', async function () { regionName: 'region', streamName: 'stream', } - const uri = createURIFromArgs(logGroupWithStream, {}) + const uri = cwlUriSchema.form({ logGroupInfo: logGroupWithStream, parameters: {} }) await copyLogResource(uri) @@ -35,7 +35,7 @@ describe('copyLogResource', async function () { groupName: 'group2', regionName: 'region2', } - const uri = createURIFromArgs(logGroup, {}) + const uri = cwlUriSchema.form({ logGroupInfo: logGroup, parameters: {} }) await copyLogResource(uri) diff --git a/packages/core/src/test/awsService/cloudWatchLogs/commands/saveCurrentLogDataContent.test.ts b/packages/core/src/test/awsService/cloudWatchLogs/commands/saveCurrentLogDataContent.test.ts index 7388b05c718..cd43c91150c 100644 --- a/packages/core/src/test/awsService/cloudWatchLogs/commands/saveCurrentLogDataContent.test.ts +++ b/packages/core/src/test/awsService/cloudWatchLogs/commands/saveCurrentLogDataContent.test.ts @@ -7,7 +7,6 @@ import assert from 'assert' import * as path from 'path' import * as vscode from 'vscode' -import { createURIFromArgs } from '../../../../awsService/cloudWatchLogs/cloudWatchLogsUtils' import { saveCurrentLogDataContent } from '../../../../awsService/cloudWatchLogs/commands/saveCurrentLogDataContent' import { fileExists, makeTemporaryToolkitFolder, readFileAsString } from '../../../../shared/filesystemUtilities' import { getTestWindow } from '../../../shared/vscode/window' @@ -18,6 +17,7 @@ import { LogDataRegistry, } from '../../../../awsService/cloudWatchLogs/registry/logDataRegistry' import { assertTextEditorContains } from '../../../testUtil' +import { cwlUriSchema } from '../../../../awsService/cloudWatchLogs/cloudWatchLogsUtils' import { fs } from '../../../../shared' async function testFilterLogEvents( @@ -55,7 +55,7 @@ describe('saveCurrentLogDataContent', async function () { regionName: 'r', streamName: 's', } - const uri = createURIFromArgs(logGroupInfo, {}) + const uri = cwlUriSchema.form({ logGroupInfo: logGroupInfo, parameters: {} }) LogDataRegistry.instance.registerInitialLog(uri, testFilterLogEvents) await LogDataRegistry.instance.fetchNextLogEvents(uri) await vscode.window.showTextDocument(uri) diff --git a/packages/core/src/test/awsService/cloudWatchLogs/document/logDataDocumentProvider.test.ts b/packages/core/src/test/awsService/cloudWatchLogs/document/logDataDocumentProvider.test.ts index 703d1f59cb0..221d0a6fee3 100644 --- a/packages/core/src/test/awsService/cloudWatchLogs/document/logDataDocumentProvider.test.ts +++ b/packages/core/src/test/awsService/cloudWatchLogs/document/logDataDocumentProvider.test.ts @@ -7,7 +7,7 @@ import assert from 'assert' import * as vscode from 'vscode' import { CloudWatchLogsSettings, - createURIFromArgs, + cwlUriSchema, isLogStreamUri, } from '../../../../awsService/cloudWatchLogs/cloudWatchLogsUtils' import { LogDataDocumentProvider } from '../../../../awsService/cloudWatchLogs/document/logDataDocumentProvider' @@ -64,7 +64,7 @@ describe('LogDataDocumentProvider', async function () { regionName: 'region', streamName: 'stream', } - const getLogsUri = createURIFromArgs(getLogsLogGroupInfo, {}) + const getLogsUri = cwlUriSchema.form({ logGroupInfo: getLogsLogGroupInfo, parameters: {} }) const filterLogsStream: CloudWatchLogsData = { events: [], @@ -77,7 +77,10 @@ describe('LogDataDocumentProvider', async function () { busy: false, } - const filterLogsUri = createURIFromArgs(filterLogsStream.logGroupInfo, filterLogsStream.parameters) + const filterLogsUri = cwlUriSchema.form({ + logGroupInfo: filterLogsStream.logGroupInfo, + parameters: filterLogsStream.parameters, + }) before(function () { config = new Settings(vscode.ConfigurationTarget.Workspace) @@ -130,7 +133,7 @@ describe('LogDataDocumentProvider', async function () { regionName: 'regionA', streamName: 'streamA', } - const logStreamNameUri = createURIFromArgs(logGroupInfo, {}) + const logStreamNameUri = cwlUriSchema.form({ logGroupInfo: logGroupInfo, parameters: {} }) const events: FilteredLogEvent[] = [ { diff --git a/packages/core/src/test/awsService/cloudWatchLogs/document/logStreamsCodeLensProvider.test.ts b/packages/core/src/test/awsService/cloudWatchLogs/document/logStreamsCodeLensProvider.test.ts index 52ab01b30bb..ae222fd25df 100644 --- a/packages/core/src/test/awsService/cloudWatchLogs/document/logStreamsCodeLensProvider.test.ts +++ b/packages/core/src/test/awsService/cloudWatchLogs/document/logStreamsCodeLensProvider.test.ts @@ -9,8 +9,8 @@ import { LogDataRegistry } from '../../../../awsService/cloudWatchLogs/registry/ import { LogDataDocumentProvider } from '../../../../awsService/cloudWatchLogs/document/logDataDocumentProvider' import { CancellationToken, CodeLens, TextDocument } from 'vscode' import assert = require('assert') -import { createURIFromArgs } from '../../../../awsService/cloudWatchLogs/cloudWatchLogsUtils' import { createStubInstance, SinonStubbedInstance } from 'sinon' +import { cwlUriSchema } from '../../../../awsService/cloudWatchLogs/cloudWatchLogsUtils' describe('LogStreamCodeLensProvider', async () => { describe('provideCodeLenses()', async () => { @@ -18,7 +18,7 @@ describe('LogStreamCodeLensProvider', async () => { let documentProvider: SinonStubbedInstance const logGroupInfo = { groupName: 'MyGroupName', regionName: 'MyRegionName' } - const logUri = createURIFromArgs(logGroupInfo, {}) + const logUri = cwlUriSchema.form({ logGroupInfo: logGroupInfo, parameters: {} }) before(async () => { const registry: LogDataRegistry = {} as LogDataRegistry diff --git a/packages/core/src/test/awsService/cloudWatchLogs/registry/logDataRegistry.test.ts b/packages/core/src/test/awsService/cloudWatchLogs/registry/logDataRegistry.test.ts index 108b6843b8a..f9b253d941f 100644 --- a/packages/core/src/test/awsService/cloudWatchLogs/registry/logDataRegistry.test.ts +++ b/packages/core/src/test/awsService/cloudWatchLogs/registry/logDataRegistry.test.ts @@ -14,7 +14,7 @@ import { CloudWatchLogsData, } from '../../../../awsService/cloudWatchLogs/registry/logDataRegistry' import { Settings } from '../../../../shared/settings' -import { CloudWatchLogsSettings, createURIFromArgs } from '../../../../awsService/cloudWatchLogs/cloudWatchLogsUtils' +import { CloudWatchLogsSettings, cwlUriSchema } from '../../../../awsService/cloudWatchLogs/cloudWatchLogsUtils' import { backwardToken, fakeGetLogEvents, @@ -34,11 +34,23 @@ describe('LogDataRegistry', async function () { const config = new Settings(vscode.ConfigurationTarget.Workspace) - const registeredUri = createURIFromArgs(testLogData.logGroupInfo, testLogData.parameters) - const unregisteredUri = createURIFromArgs(unregisteredData.logGroupInfo, unregisteredData.parameters) - const newLineUri = createURIFromArgs(newLineData.logGroupInfo, newLineData.parameters) - const searchLogGroupUri = createURIFromArgs(logGroupsData.logGroupInfo, logGroupsData.parameters) - const paginatedUri = createURIFromArgs(paginatedData.logGroupInfo, paginatedData.parameters) + const registeredUri = cwlUriSchema.form({ + logGroupInfo: testLogData.logGroupInfo, + parameters: testLogData.parameters, + }) + const unregisteredUri = cwlUriSchema.form({ + logGroupInfo: unregisteredData.logGroupInfo, + parameters: unregisteredData.parameters, + }) + const newLineUri = cwlUriSchema.form({ logGroupInfo: newLineData.logGroupInfo, parameters: newLineData.parameters }) + const searchLogGroupUri = cwlUriSchema.form({ + logGroupInfo: logGroupsData.logGroupInfo, + parameters: logGroupsData.parameters, + }) + const paginatedUri = cwlUriSchema.form({ + logGroupInfo: paginatedData.logGroupInfo, + parameters: paginatedData.parameters, + }) /** * Only intended to expose the {get|set}LogData methods for testing purposes. diff --git a/packages/core/src/test/awsService/cloudWatchLogs/utils.test.ts b/packages/core/src/test/awsService/cloudWatchLogs/utils.test.ts index eed2b709c40..02a768c4f07 100644 --- a/packages/core/src/test/awsService/cloudWatchLogs/utils.test.ts +++ b/packages/core/src/test/awsService/cloudWatchLogs/utils.test.ts @@ -6,11 +6,7 @@ import assert from 'assert' import { CloudWatchLogs } from 'aws-sdk' import * as vscode from 'vscode' -import { - createURIFromArgs, - parseCloudWatchLogsUri, - uriToKey, -} from '../../../awsService/cloudWatchLogs/cloudWatchLogsUtils' +import { cwlUriSchema, uriToKey } from '../../../awsService/cloudWatchLogs/cloudWatchLogsUtils' import { CloudWatchLogsParameters, CloudWatchLogsData, @@ -170,28 +166,31 @@ export const paginatedData: CloudWatchLogsData = { retrieveLogsFunction: returnPaginatedEvents, busy: false, } -export const goodUri = createURIFromArgs(testComponents.logGroupInfo, testComponents.parameters) +export const goodUri = cwlUriSchema.form({ + logGroupInfo: testComponents.logGroupInfo, + parameters: testComponents.parameters, +}) describe('parseCloudWatchLogsUri', async function () { it('converts a valid URI to components', function () { - const result = parseCloudWatchLogsUri(goodUri) + const result = cwlUriSchema.parse(goodUri) assert.deepStrictEqual(result.logGroupInfo, testComponents.logGroupInfo) assert.deepStrictEqual(result.parameters, testComponents.parameters) }) it('does not convert URIs with an invalid scheme', async function () { assert.throws(() => { - parseCloudWatchLogsUri(vscode.Uri.parse('wrong:scheme')) + cwlUriSchema.parse(vscode.Uri.parse('wrong:scheme')) }) }) it('does not convert URIs with more or less than three elements', async function () { assert.throws(() => { - parseCloudWatchLogsUri(vscode.Uri.parse(`${CLOUDWATCH_LOGS_SCHEME}:elementOne:elementTwo`)) + cwlUriSchema.parse(vscode.Uri.parse(`${CLOUDWATCH_LOGS_SCHEME}:elementOne:elementTwo`)) }) assert.throws(() => { - parseCloudWatchLogsUri( + cwlUriSchema.parse( vscode.Uri.parse(`${CLOUDWATCH_LOGS_SCHEME}:elementOne:elementTwo:elementThree:whoopsAllElements`) ) }) @@ -208,7 +207,7 @@ describe('createURIFromArgs', function () { )}` ) assert.deepStrictEqual(testUri, goodUri) - const newTestComponents = parseCloudWatchLogsUri(testUri) + const newTestComponents = cwlUriSchema.parse(testUri) assert.deepStrictEqual(testComponents, newTestComponents) }) }) @@ -230,8 +229,8 @@ describe('uriToKey', function () { it('creates the same key for different order query', function () { const param1: CloudWatchLogsParameters = { filterPattern: 'same', startTime: 0 } const param2: CloudWatchLogsParameters = { startTime: 0, filterPattern: 'same' } - const firstOrder = createURIFromArgs(testComponents.logGroupInfo, param1) - const secondOrder = createURIFromArgs(testComponents.logGroupInfo, param2) + const firstOrder = cwlUriSchema.form({ logGroupInfo: testComponents.logGroupInfo, parameters: param1 }) + const secondOrder = cwlUriSchema.form({ logGroupInfo: testComponents.logGroupInfo, parameters: param2 }) assert.notDeepStrictEqual(firstOrder, secondOrder) assert.strictEqual(uriToKey(firstOrder), uriToKey(secondOrder)) diff --git a/packages/core/src/test/awsService/ec2/ec2LogDocumentProvider.test.ts b/packages/core/src/test/awsService/ec2/ec2LogDocumentProvider.test.ts new file mode 100644 index 00000000000..3889be0ac9f --- /dev/null +++ b/packages/core/src/test/awsService/ec2/ec2LogDocumentProvider.test.ts @@ -0,0 +1,39 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import * as vscode from 'vscode' +import * as sinon from 'sinon' +import { Ec2LogDocumentProvider } from '../../../awsService/ec2/ec2LogDocumentProvider' +import { ec2LogsScheme } from '../../../shared/constants' +import { Ec2Client } from '../../../shared/clients/ec2Client' + +describe('LogDataDocumentProvider', async function () { + let provider: Ec2LogDocumentProvider + + before(function () { + provider = new Ec2LogDocumentProvider() + }) + + it('throws error on attempt to get content from other schemes', async function () { + const wrongSchemeUri = vscode.Uri.parse(`ec2-not:us-west1:id`) + + await assert.rejects(async () => await provider.provideTextDocumentContent(wrongSchemeUri), { + message: `Invalid EC2 Logs URI: ${wrongSchemeUri.toString()}`, + }) + }) + + it('fetches content for valid ec2 log URI', async function () { + const validUri = vscode.Uri.parse(`${ec2LogsScheme}:us-west1:instance1`) + const expectedContent = 'log content' + sinon.stub(Ec2Client.prototype, 'getConsoleOutput').resolves({ + InstanceId: 'instance1', + Output: expectedContent, + }) + const content = await provider.provideTextDocumentContent(validUri) + assert.strictEqual(content, expectedContent) + sinon.restore() + }) +}) diff --git a/packages/core/src/test/setupUtil.ts b/packages/core/src/test/setupUtil.ts index 6a46514cf06..f158d67ad55 100644 --- a/packages/core/src/test/setupUtil.ts +++ b/packages/core/src/test/setupUtil.ts @@ -11,6 +11,7 @@ import { hasKey } from '../shared/utilities/tsUtils' import { getTestWindow, printPendingUiElements } from './shared/vscode/window' import { ToolkitError, formatError } from '../shared/errors' import { proceedToBrowser } from '../auth/sso/model' +import { decodeBase64 } from '../shared' const runnableTimeout = Symbol('runnableTimeout') @@ -164,7 +165,7 @@ export async function invokeLambda(id: string, request: unknown): Promise