Skip to content

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

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

Merged
merged 5 commits into from
Feb 26, 2025
Merged
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'
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 @@ -4,21 +4,17 @@
*/
import * as nodefs from 'fs' // eslint-disable-line no-restricted-imports
import { ToolkitError } from '../../../shared/errors'
import path from 'path'

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 All @@ -32,6 +28,14 @@ export interface ProjectMetadata {
export class MetadataManager {
private static instance: MetadataManager
private metadata: ProjectMetadata | undefined
private static readonly metadataPath = path.join(
path.resolve(__dirname, '../../../../../'),
Copy link
Contributor

Choose a reason for hiding this comment

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

you're probably going to wait to integrate with our copyFiles scripts. This path probably exists in the extension debug mode but I don't think it exists in the actual build. Instead, you will need to copy this file to the resources file and pull from that

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I have made the changes in the path with the copyFiles scripts in the next PR and it is working correctly too.

'src',
'awsService',
'appBuilder',
'serverlessLand',
'metadata.json'
)

private constructor() {}

Expand All @@ -42,6 +46,14 @@ export class MetadataManager {
return MetadataManager.instance
}

public static initialize(): MetadataManager {
const instance = MetadataManager.getInstance()
instance.loadMetadata(MetadataManager.metadataPath).catch((err) => {
throw new ToolkitError(`Failed to load metadata: ${err}`)
})
return instance
}

/**
* Loads metadata from a JSON file
* @param metadataPath Path to the metadata JSON file
Expand Down Expand Up @@ -96,11 +108,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 +124,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
Loading