Skip to content

Commit 10a1e0b

Browse files
vicheeychungjac
andauthored
feat(appbuilder): add CapacityProvider node and property node (#8395)
## Problem LMI function contains new property and we have new CapacityProvider property ## Solution - Add support for `AWS::Serverless::CapacityProvider` resource type in explorer - Display capacity provider properties in resource nodes - Stringify CloudFormation intrinsic functions in property nodes for better readability - Add type-safe resource entity interfaces (FunctionResourceEntity, CapacityProviderResourceEntity) - Add type guards (isFunctionResource, isCapacityProviderResource) for safer resource handling - Hide property node if not defined - Fix optional chaining for Architectures array access to prevent undefined errors - Add test coverage for capacity provider resources with intrinsic functions --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license. --------- Co-authored-by: chungjac <chungjac@amazon.com>
1 parent 151f9f0 commit 10a1e0b

File tree

23 files changed

+750
-44
lines changed

23 files changed

+750
-44
lines changed

packages/core/src/awsService/appBuilder/explorer/nodes/deployedNode.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,11 @@ import {
2626
SERVERLESS_FUNCTION_TYPE,
2727
SERVERLESS_API_TYPE,
2828
s3BucketType,
29+
SERVERLESS_CAPACITY_PROVIDER_TYPE,
2930
} from '../../../../shared/cloudformation/cloudformation'
3031
import { ToolkitError } from '../../../../shared/errors'
31-
import { ResourceTreeEntity } from '../samProject'
32+
import { ResourceTreeEntity, isFunctionResource } from '../samProject'
33+
import { LambdaCapacityProviderNode } from '../../../../lambda/explorer/lambdaCapacityProviderNode'
3234

3335
const localize = nls.loadMessageBundle()
3436
export interface DeployedResource {
@@ -43,6 +45,7 @@ export const DeployedResourceContextValues: Record<string, string> = {
4345
[SERVERLESS_FUNCTION_TYPE]: 'awsRegionFunctionNodeDownloadable',
4446
[SERVERLESS_API_TYPE]: 'awsApiGatewayNode',
4547
[s3BucketType]: 'awsS3BucketNode',
48+
[SERVERLESS_CAPACITY_PROVIDER_TYPE]: 'awsCapacityProviderNode',
4649
}
4750

4851
export class DeployedResourceNode implements TreeNode<DeployedResource> {
@@ -93,12 +96,13 @@ export async function generateDeployedNode(
9396
try {
9497
configuration = (await defaultClient.getFunction(deployedResource.PhysicalResourceId))
9598
.Configuration as FunctionConfiguration
99+
const codeUri = isFunctionResource(resourceTreeEntity) ? resourceTreeEntity.CodeUri : undefined
96100
newDeployedResource = new LambdaFunctionNode(
97101
lambdaNode,
98102
regionCode,
99103
configuration,
100104
undefined,
101-
location ? vscode.Uri.joinPath(location, resourceTreeEntity.CodeUri ?? '').fsPath : undefined,
105+
location ? vscode.Uri.joinPath(location, codeUri ?? '').fsPath : undefined,
102106
location,
103107
deployedResource.LogicalResourceId
104108
)
@@ -124,6 +128,10 @@ export async function generateDeployedNode(
124128
newDeployedResource = new RestApiNode(apiParentNode, partitionId, regionCode, apiNode as RestApi)
125129
break
126130
}
131+
case SERVERLESS_CAPACITY_PROVIDER_TYPE: {
132+
newDeployedResource = new LambdaCapacityProviderNode(regionCode, deployedResource)
133+
break
134+
}
127135
default:
128136
newDeployedResource = new DeployedResourceNode(deployedResource)
129137
getLogger().info('Details are missing or are incomplete for: %O', deployedResource)

packages/core/src/awsService/appBuilder/explorer/nodes/propertyNode.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@ import * as vscode from 'vscode'
77
import { getIcon } from '../../../../shared/icons'
88
import { TreeNode } from '../../../../shared/treeview/resourceTreeDataProvider'
99

10+
/**
11+
* Formats CloudFormation intrinsic functions into readable strings
12+
*/
13+
function formatIntrinsicFunction(value: any): string | undefined {
14+
if (typeof value !== 'object' || value === null || Object.keys(value).length !== 1) {
15+
return undefined
16+
}
17+
return JSON.stringify(value)
18+
}
19+
1020
export class PropertyNode implements TreeNode {
1121
public readonly id = this.key
1222
public readonly resource = this.value
@@ -25,12 +35,15 @@ export class PropertyNode implements TreeNode {
2535
}
2636

2737
public getTreeItem() {
28-
const item = new vscode.TreeItem(`${this.key}: ${this.value}`)
38+
const intrinsicFormat = formatIntrinsicFunction(this.value)
39+
const displayValue = intrinsicFormat ?? this.value
40+
41+
const item = new vscode.TreeItem(`${this.key}: ${displayValue}`)
2942

3043
item.contextValue = 'awsAppBuilderPropertyNode'
3144
item.iconPath = getIcon('vscode-gear')
3245

33-
if (this.value instanceof Array || this.value instanceof Object) {
46+
if (!intrinsicFormat && (this.value instanceof Array || this.value instanceof Object)) {
3447
item.label = this.key
3548
item.collapsibleState = vscode.TreeItemCollapsibleState.Collapsed
3649
}
@@ -41,6 +54,6 @@ export class PropertyNode implements TreeNode {
4154

4255
export function generatePropertyNodes(properties: { [key: string]: any }): TreeNode[] {
4356
return Object.entries(properties)
44-
.filter(([key, _]) => key !== 'Id' && key !== 'Type' && key !== 'Events')
57+
.filter(([key, value]) => key !== 'Id' && key !== 'Type' && key !== 'Events' && value !== undefined)
4558
.map(([key, value]) => new PropertyNode(key, value))
4659
}

packages/core/src/awsService/appBuilder/explorer/nodes/resourceNode.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
s3BucketType,
1313
appRunnerType,
1414
ecrRepositoryType,
15+
SERVERLESS_CAPACITY_PROVIDER_TYPE,
1516
} from '../../../../shared/cloudformation/cloudformation'
1617
import { generatePropertyNodes } from './propertyNode'
1718
import { generateDeployedNode } from './deployedNode'
@@ -24,6 +25,7 @@ enum ResourceTypeId {
2425
Function = 'function',
2526
DeployedFunction = 'deployed-function',
2627
Api = 'api',
28+
CapacityProvider = 'capacityprovider',
2729
Other = '',
2830
}
2931

@@ -88,7 +90,10 @@ export class ResourceNode implements TreeNode {
8890
this.location.projectRoot
8991
)) as DeployedResourceNode[]
9092
}
91-
if (this.resourceTreeEntity.Type === SERVERLESS_FUNCTION_TYPE) {
93+
if (
94+
this.resourceTreeEntity.Type === SERVERLESS_FUNCTION_TYPE ||
95+
this.resourceTreeEntity.Type === SERVERLESS_CAPACITY_PROVIDER_TYPE
96+
) {
9297
propertyNodes = generatePropertyNodes(this.resourceTreeEntity)
9398
}
9499

@@ -132,6 +137,8 @@ export class ResourceNode implements TreeNode {
132137
return getIcon('aws-apprunner-service')
133138
case ecrRepositoryType:
134139
return getIcon('aws-ecr-registry')
140+
case SERVERLESS_CAPACITY_PROVIDER_TYPE:
141+
return getIcon('vscode-gear')
135142
default:
136143
return getIcon('vscode-info')
137144
}
@@ -146,6 +153,8 @@ export class ResourceNode implements TreeNode {
146153
return ResourceTypeId.Function
147154
case 'Api':
148155
return ResourceTypeId.Api
156+
case SERVERLESS_CAPACITY_PROVIDER_TYPE:
157+
return ResourceTypeId.CapacityProvider
149158
default:
150159
return ResourceTypeId.Other
151160
}

packages/core/src/awsService/appBuilder/explorer/samProject.ts

Lines changed: 65 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import { SamConfig, SamConfigErrorCode } from '../../../shared/sam/config'
99
import { getLogger } from '../../../shared/logger/logger'
1010
import { ToolkitError } from '../../../shared/errors'
1111
import { showViewLogsMessage } from '../../../shared/utilities/messages'
12-
import { Runtime } from '@aws-sdk/client-lambda'
1312

1413
export interface SamApp {
1514
location: SamAppLocation
@@ -22,18 +21,40 @@ export interface SamAppLocation {
2221
projectRoot: vscode.Uri
2322
}
2423

25-
export interface ResourceTreeEntity {
24+
export interface BaseResourceEntity {
2625
Id: string
2726
Type: string
28-
Runtime?: Runtime
29-
CodeUri?: string
30-
Handler?: string
31-
Events?: ResourceTreeEntity[]
27+
}
28+
29+
export interface EventEntity extends BaseResourceEntity {
3230
Path?: string
3331
Method?: string
32+
}
33+
34+
export interface FunctionResourceEntity extends BaseResourceEntity {
35+
Runtime?: string
36+
CodeUri?: string
37+
Handler?: string
38+
Events?: EventEntity[]
3439
Environment?: {
3540
Variables: Record<string, any>
3641
}
42+
CapacityProviderConfig?: string
43+
Architectures?: string
44+
}
45+
46+
export interface CapacityProviderResourceEntity extends BaseResourceEntity {
47+
Architectures?: string
48+
}
49+
50+
export type ResourceTreeEntity = FunctionResourceEntity | CapacityProviderResourceEntity | BaseResourceEntity
51+
52+
export function isFunctionResource(resource: ResourceTreeEntity): resource is FunctionResourceEntity {
53+
return resource.Type === CloudFormation.SERVERLESS_FUNCTION_TYPE
54+
}
55+
56+
export function isCapacityProviderResource(resource: ResourceTreeEntity): resource is CapacityProviderResourceEntity {
57+
return resource.Type === CloudFormation.SERVERLESS_CAPACITY_PROVIDER_TYPE
3758
}
3859

3960
export async function getStackName(projectRoot: vscode.Uri): Promise<any> {
@@ -78,30 +99,58 @@ function getResourceEntity(template: any): ResourceTreeEntity[] {
7899
const resourceTree: ResourceTreeEntity[] = []
79100

80101
for (const [logicalId, resource] of Object.entries(template?.Resources ?? []) as [string, any][]) {
81-
const resourceEntity: ResourceTreeEntity = {
82-
Id: logicalId,
83-
Type: resource.Type,
102+
const resourceEntity = createResourceEntity(logicalId, resource, template)
103+
resourceTree.push(resourceEntity)
104+
}
105+
return resourceTree
106+
}
107+
108+
function createResourceEntity(logicalId: string, resource: any, template: any): ResourceTreeEntity {
109+
const baseEntity: BaseResourceEntity = {
110+
Id: logicalId,
111+
Type: resource.Type,
112+
}
113+
114+
// Create type-specific entities
115+
if (resource.Type === CloudFormation.SERVERLESS_FUNCTION_TYPE) {
116+
const functionEntity: FunctionResourceEntity = {
117+
...baseEntity,
84118
Runtime: resource.Properties?.Runtime ?? template?.Globals?.Function?.Runtime,
85119
Handler: resource.Properties?.Handler ?? template?.Globals?.Function?.Handler,
86120
Events: resource.Properties?.Events ? getEvents(resource.Properties.Events) : undefined,
87121
CodeUri: resource.Properties?.CodeUri ?? template?.Globals?.Function?.CodeUri,
88122
Environment: resource.Properties?.Environment ?? template?.Globals?.Function?.Environment,
123+
CapacityProviderConfig:
124+
resource.Properties?.CapacityProviderConfig ?? template?.Globals?.Function?.CapacityProviderConfig,
125+
Architectures: resource.Properties?.Architectures?.[0] ?? template?.Globals?.Function?.Architectures?.[0],
89126
}
90-
resourceTree.push(resourceEntity)
127+
return functionEntity
91128
}
92-
return resourceTree
129+
130+
if (resource.Type === CloudFormation.SERVERLESS_CAPACITY_PROVIDER_TYPE) {
131+
const capacityProviderEntity: CapacityProviderResourceEntity = {
132+
...baseEntity,
133+
Architectures:
134+
resource.Properties?.InstanceRequirements?.Architectures?.[0] ??
135+
template?.Globals?.CapacityProvider?.InstanceRequirements?.Architectures?.[0],
136+
}
137+
return capacityProviderEntity
138+
}
139+
140+
// Generic resource for unsupported types
141+
return baseEntity
93142
}
94143

95-
function getEvents(events: Record<string, any>): ResourceTreeEntity[] {
96-
const eventResources: ResourceTreeEntity[] = []
144+
function getEvents(events: Record<string, any>): EventEntity[] {
145+
const eventResources: EventEntity[] = []
97146

98147
for (const [eventsLogicalId, event] of Object.entries(events)) {
99148
const eventProperties = event.Properties
100-
const eventResource: ResourceTreeEntity = {
149+
const eventResource: EventEntity = {
101150
Id: eventsLogicalId,
102151
Type: event.Type,
103-
Path: eventProperties.Path,
104-
Method: eventProperties.Method,
152+
Path: eventProperties?.Path,
153+
Method: eventProperties?.Method,
105154
}
106155
eventResources.push(eventResource)
107156
}

packages/core/src/awsService/appBuilder/utils.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { TreeNode } from '../../shared/treeview/resourceTreeDataProvider'
88
import * as nls from 'vscode-nls'
99
import { ResourceNode } from './explorer/nodes/resourceNode'
1010
import type { SamAppLocation } from './explorer/samProject'
11+
import { isFunctionResource } from './explorer/samProject'
1112
import { ToolkitError } from '../../shared/errors'
1213
import globals from '../../shared/extensionGlobals'
1314
import { OpenTemplateParams, OpenTemplateWizard } from './explorer/openTemplate'
@@ -552,27 +553,33 @@ export async function runOpenTemplate(arg?: TreeNode) {
552553
*/
553554
export async function runOpenHandler(arg: ResourceNode): Promise<void> {
554555
const folderUri = path.dirname(arg.resource.location.fsPath)
555-
if (!arg.resource.resource.CodeUri) {
556+
const resource = arg.resource.resource
557+
558+
if (!isFunctionResource(resource)) {
559+
throw new ToolkitError('Resource is not a Lambda function', { code: 'NotAFunction' })
560+
}
561+
562+
if (!resource.CodeUri) {
556563
throw new ToolkitError('No CodeUri provided in template, cannot open handler', { code: 'NoCodeUriProvided' })
557564
}
558565

559-
if (!arg.resource.resource.Handler) {
566+
if (!resource.Handler) {
560567
throw new ToolkitError('No Handler provided in template, cannot open handler', { code: 'NoHandlerProvided' })
561568
}
562569

563-
if (!arg.resource.resource.Runtime) {
570+
if (!resource.Runtime) {
564571
throw new ToolkitError('No Runtime provided in template, cannot open handler', { code: 'NoRuntimeProvided' })
565572
}
566573

567574
const handlerFile = await getLambdaHandlerFile(
568575
vscode.Uri.file(folderUri),
569-
arg.resource.resource.CodeUri,
570-
arg.resource.resource.Handler,
571-
arg.resource.resource.Runtime
576+
resource.CodeUri,
577+
resource.Handler,
578+
resource.Runtime as Runtime
572579
)
573580
if (!handlerFile) {
574581
throw new ToolkitError(
575-
`No handler file found with name "${arg.resource.resource.Handler}". Ensure the file exists in the expected location."`,
582+
`No handler file found with name "${resource.Handler}". Ensure the file exists in the expected location."`,
576583
{
577584
code: 'NoHandlerFound',
578585
}

packages/core/src/lambda/activation.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,6 @@ export async function activate(context: ExtContext): Promise<void> {
233233
Commands.register('aws.launchDebugConfigForm', async (node: ResourceNode) =>
234234
registerSamDebugInvokeVueCommand(context.extensionContext, { resource: node })
235235
),
236-
237236
Commands.register('aws.appBuilder.tailLogs', async (node: LambdaFunctionNode | TreeNode) => {
238237
let functionConfiguration: FunctionConfiguration
239238
try {
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import * as vscode from 'vscode'
7+
import { getIcon } from '../../shared/icons'
8+
9+
import { AWSResourceNode } from '../../shared/treeview/nodes/awsResourceNode'
10+
import { AWSTreeNodeBase } from '../../shared/treeview/nodes/awsTreeNodeBase'
11+
import globals from '../../shared/extensionGlobals'
12+
import { ToolkitError } from '../../shared/errors'
13+
14+
export const contextValueLambdaCapacityProvider = 'awsCapacityProviderNode'
15+
16+
export class LambdaCapacityProviderNode extends AWSTreeNodeBase implements AWSResourceNode {
17+
public constructor(
18+
public override readonly regionCode: string,
19+
public readonly deployedResource: any,
20+
public override readonly contextValue?: string
21+
) {
22+
super(
23+
deployedResource.LogicalResourceId,
24+
contextValue === contextValueLambdaCapacityProvider
25+
? vscode.TreeItemCollapsibleState.Collapsed
26+
: vscode.TreeItemCollapsibleState.None
27+
)
28+
this.iconPath = getIcon('vscode-gear')
29+
this.contextValue = contextValueLambdaCapacityProvider
30+
}
31+
32+
public get name() {
33+
return this.deployedResource.LogicalResourceId
34+
}
35+
private get accountId(): string {
36+
const accountId = globals.awsContext.getCredentialAccountId()
37+
if (!accountId) {
38+
throw new ToolkitError('Aws account ID not found')
39+
}
40+
return accountId
41+
}
42+
43+
public get arn() {
44+
return `arn:aws:lambda:${this.regionCode}:${this.accountId}:capacity-provider:${this.deployedResource.PhysicalResourceId}`
45+
}
46+
}

packages/core/src/lambda/vue/configEditor/samInvokeBackend.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import * as CloudFormation from '../../../shared/cloudformation/cloudformation'
2828
import { openLaunchJsonFile } from '../../../shared/sam/debugger/commands/addSamDebugConfiguration'
2929
import { getSampleLambdaPayloads } from '../../utils'
3030
import { samLambdaCreatableRuntimes } from '../../models/samLambdaRuntime'
31+
import { isFunctionResource } from '../../../awsService/appBuilder/explorer/samProject'
3132
import globals from '../../../shared/extensionGlobals'
3233
import { VueWebview } from '../../../webviews/main'
3334
import { Commands } from '../../../shared/vscode/commands2'
@@ -441,6 +442,10 @@ export async function registerSamDebugInvokeVueCommand(
441442
(config) => (config.invokeTarget as TemplateTargetProperties).logicalId === resource.resource.Id
442443
)
443444

445+
if (!isFunctionResource(resource.resource)) {
446+
throw new ToolkitError('Resource is not a Lambda function')
447+
}
448+
444449
const webview = new WebviewPanel(context, launchConfig, {
445450
logicalId: resource.resource.Id ?? '',
446451
region: resource.region ?? '',

packages/core/src/shared/cloudformation/cloudformation.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ export const LAMBDA_FUNCTION_TYPE = 'AWS::Lambda::Function' // eslint-disable-li
1919
export const LAMBDA_LAYER_TYPE = 'AWS::Lambda::LayerVersion' // eslint-disable-line @typescript-eslint/naming-convention
2020
export const LAMBDA_URL_TYPE = 'AWS::Lambda::Url' // eslint-disable-line @typescript-eslint/naming-convention
2121
export const SERVERLESS_LAYER_TYPE = 'AWS::Serverless::LayerVersion' // eslint-disable-line @typescript-eslint/naming-convention
22+
export const SERVERLESS_CAPACITY_PROVIDER_TYPE = 'AWS::Serverless::CapacityProvider' // eslint-disable-line @typescript-eslint/naming-convention
23+
export const LAMBDA_CAPACITY_PROVIDER_TYPE = 'AWS::Lambda::CapacityProvider' // eslint-disable-line @typescript-eslint/naming-convention
2224

2325
export const serverlessTableType = 'AWS::Serverless::SimpleTable'
2426
export const s3BucketType = 'AWS::S3::Bucket'

0 commit comments

Comments
 (0)