Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(lambda): download serverless land patterns in IDE #6612

Open
wants to merge 5 commits into
base: feature/serverlessland
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 79 additions & 15 deletions packages/core/src/awsService/appBuilder/serverlessLand/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,22 @@
import * as nls from 'vscode-nls'
const localize = nls.loadMessageBundle()
import * as path from 'path'
import * as vscode from 'vscode'
import { getTelemetryReason, getTelemetryResult } from '../../../shared/errors'
import { getLogger } from '../../../shared/logger/logger'
import globals from '../../../shared/extensionGlobals'
import { checklogs } from '../../../shared/localizedText'
import { Result, telemetry } from '../../../shared/telemetry/telemetry'
import { CreateServerlessLandWizard } from './wizard'
import { CreateServerlessLandWizardForm, CreateServerlessLandWizard } from './wizard'
import { ExtContext } from '../../../shared/extensions'
import { addFolderToWorkspace } from '../../../shared/utilities/workspaceUtils'
import { ToolkitError } from '../../../shared/errors'
import { fs } from '../../../shared/fs/fs'
import { getPattern } from '../../../shared/utilities/downloadPatterns'
import { MetadataManager } from './metadataManager'

export const readmeFile: string = 'README.md'
const serverlessLandOwner = 'aws-samples'
const serverlessLandRepo = 'serverless-patterns'

/**
* Creates a new Serverless Land project using the provided extension context
Expand All @@ -31,38 +37,33 @@ export const readmeFile: string = 'README.md'
* 6. Handles errors and emits telemetry
*/
export async function createNewServerlessLandProject(extContext: ExtContext): Promise<void> {
const awsContext = extContext.awsContext
let createResult: Result = 'Succeeded'
let reason: string | undefined
let metadataManager: MetadataManager

try {
const credentials = await awsContext.getCredentials()
const defaultRegion = awsContext.getCredentialDefaultRegion()

metadataManager = MetadataManager.getInstance()
// Launch the project creation wizard
const config = await new CreateServerlessLandWizard({
credentials,
defaultRegion,
}).run()
const config = await launchProjectCreationWizard(extContext)
if (!config) {
createResult = 'Cancelled'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the the purpose behind setting value to these two local variables right before returning?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The two values will be emitted in the telemetry, within the finally block of the try-catch statement.

reason = 'userCancelled'
return
}
const assetName = metadataManager.getAssetName(config.pattern, config.runtime, config.iac)

// Add the project folder to the workspace
await downloadPatternCode(config, assetName)
await openReadmeFile(config)
await addFolderToWorkspace(
{
uri: config.location,
name: path.basename(config.location.fsPath),
uri: vscode.Uri.joinPath(config.location, config.name),
name: path.basename(config.name),
},
true
)
} catch (err) {
createResult = getTelemetryResult(err)
reason = getTelemetryReason(err)

globals.outputChannel.show(true)
getLogger().error(
localize(
'AWS.serverlessland.initWizard.general.error',
Expand All @@ -80,3 +81,66 @@ export async function createNewServerlessLandProject(extContext: ExtContext): Pr
})
}
}

async function launchProjectCreationWizard(
extContext: ExtContext
): Promise<CreateServerlessLandWizardForm | undefined> {
const awsContext = extContext.awsContext
const credentials = await awsContext.getCredentials()
const defaultRegion = awsContext.getCredentialDefaultRegion()

return new CreateServerlessLandWizard({
credentials,
defaultRegion,
}).run()
}

async function downloadPatternCode(config: CreateServerlessLandWizardForm, assetName: string): Promise<void> {
const fullAssetName = assetName + '.zip'
const location = vscode.Uri.joinPath(config.location, config.name)
try {
await getPattern(serverlessLandOwner, serverlessLandRepo, fullAssetName, location, true)
} catch (error) {
if (error instanceof ToolkitError) {
throw error
}
throw new ToolkitError(`Failed to download pattern: ${error}`)
}
}

async function openReadmeFile(config: CreateServerlessLandWizardForm): Promise<void> {
try {
const readmeUri = await getProjectUri(config, readmeFile)
if (!readmeUri) {
getLogger().warn('README.md file not found in the project directory')
return
}

await vscode.commands.executeCommand('workbench.action.focusFirstEditorGroup')
await vscode.window.showTextDocument(readmeUri)
} catch (err) {
getLogger().error(`Error in openReadmeFile: ${err}`)
throw new ToolkitError('Error processing README file')
}
}

async function getProjectUri(
config: Pick<CreateServerlessLandWizardForm, 'location' | 'name'>,
file: string
): Promise<vscode.Uri | undefined> {
if (!file) {
throw Error('expected "file" parameter to have at least one item')
}
const cfnTemplatePath = path.resolve(config.location.fsPath, config.name, file)
if (await fs.exists(cfnTemplatePath)) {
return vscode.Uri.file(cfnTemplatePath)
}
void vscode.window.showWarningMessage(
localize(
'AWS.serverlessLand.initWizard.source.error.notFound',
'Project created successfully, but {0} file not found: {1}',
file!,
cfnTemplatePath!
)
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,56 +3,52 @@
"s3-lambda-resizing-sam": {
"name": "Resizing image",
"description": "Lambda, S3 • Python, Javascript, Java, .NET • SAM",
"runtimes": [
"implementation": [
{
"id": "python",
"name": "Python"
"iac": "sam",
"runtime": "python",
"assetName": "s3-lambda-resizing-python"
},
{
"id": "javascript",
"name": "Javascript"
"iac": "sam",
"runtime": "javascript",
"assetName": "s3-lambda"
},
{
"id": "dotnet",
"name": "Dotnet"
"iac": "sam",
"runtime": "java",
"assetName": "s3-lambda-resizing-java"
},
{
"id": "java",
"name": "Java"
}
],
"iac": [
{
"id": "sam",
"name": "SAM"
"iac": "sam",
"runtime": "dotnet",
"assetName": "s3-lambda-dotnet"
}
]
},
"apigw-rest-api-lambda-sam": {
"name": "Rest API",
"description": "Lambda, API Gateway • Python, Javascript, Java, .NET • SAM",
"runtimes": [
"implementation": [
{
"id": "python",
"name": "Python"
"iac": "sam",
"runtime": "python",
"assetName": "apigw-rest-api-lambda-python"
},
{
"id": "javascript",
"name": "Javascript"
"iac": "sam",
"runtime": "javascript",
"assetName": "apigw-rest-api-lambda-node"
},
{
"id": "dotnet",
"name": "Dotnet"
"iac": "sam",
"runtime": "java",
"assetName": "apigw-rest-api-lambda-java"
},
{
"id": "java",
"name": "Java"
}
],
"iac": [
{
"id": "sam",
"name": "AWS SAM"
"iac": "sam",
"runtime": "dotnet",
"assetName": "apigw-rest-api-lambda-dotnet"
}
]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,15 @@
import * as nodefs from 'fs' // eslint-disable-line no-restricted-imports
import { ToolkitError } from '../../../shared/errors'

interface IaC {
id: string
name: string
}
interface Runtime {
id: string
name: string
version: string
interface Implementation {
iac: string
runtime: string
assetName: string
}
interface PatternData {
name: string
description: string
runtimes: Runtime[]
iac: IaC[]
implementation: Implementation[]
}

export interface ProjectMetadata {
Expand Down Expand Up @@ -96,11 +91,12 @@ export class MetadataManager {
*/
public getRuntimes(pattern: string): { label: string }[] {
const patternData = this.metadata?.patterns?.[pattern]
if (!patternData || !patternData.runtimes) {
if (!patternData || !patternData.implementation) {
return []
}
return patternData.runtimes.map((runtime) => ({
label: runtime.name,
const uniqueRuntimes = new Set(patternData.implementation.map((item) => item.runtime))
return Array.from(uniqueRuntimes).map((runtime) => ({
label: runtime,
}))
}

Expand All @@ -111,11 +107,22 @@ export class MetadataManager {
*/
public getIacOptions(pattern: string): { label: string }[] {
const patternData = this.metadata?.patterns?.[pattern]
if (!patternData || !patternData.iac) {
if (!patternData || !patternData.implementation) {
return []
}
return patternData.iac.map((iac) => ({
label: iac.name,
const uniqueIaCs = new Set(patternData.implementation.map((item) => item.iac))
return Array.from(uniqueIaCs).map((iac) => ({
label: iac,
}))
}
public getAssetName(selectedPattern: string, selectedRuntime: string, selectedIaC: string): string {
const patternData = this.metadata?.patterns?.[selectedPattern]
if (!patternData || !patternData.implementation) {
return ''
}
const matchingImplementation = patternData.implementation.find(
(impl) => impl.runtime === selectedRuntime && impl.iac === selectedIaC
)
return matchingImplementation?.assetName ?? ''
}
}
Loading