Skip to content

Commit 371d23b

Browse files
kddejongmanodnyab
andauthored
feat(cloudformation): Update CloudFormation feature with latest changes (#8303)
Includes all CloudFormation improvements and fixes from November 9th through November 17th: - Integration tests and LSP server workflow scripts - Online error handling improvements - CommaDelimitedList validation and boolean support - Restart server command fixes and autocomplete improvements - CFN init typing and consolidated commands - LSP logs and exception handling - Pagination and search in import/clone commands - Resource type request handling - Environment command error handling - Project creation UI improvements - LSP integration tests for offline features - Reference-counted status bar implementation - Expand/collapse arrow and resource messaging updates - Stack events and resources loading improvements - Validation workflow enhancements - Resource management and provider naming - Non-blocking CFN operations and restart LSP functionality - Deep linking to stack information views - E2E test infrastructure setup - Deployment workflow alignment - Cached CFN server offline support - LSP installation process updates - Startup fixes and permission handling - AWS Explorer CFN panel integration - Deployment mode prompts and stack view coordination - Overview integration and parameter extraction fixes - Panel metrics and telemetry improvements - S3 upload support and file selection - Icon updates and environment warnings - Command palette fixes and document management - Related resources workflow improvements - Windows target fixes and validation improvements - Environment naming and resource state management - Stack description and telemetry configuration - Deployment/validation icons and usage tracking - Permission handling and resource loading - Environment file selection and experimental flag removal - Endpoint configuration and ChangeSet diff redesign - S3 upload validation and inline completion removal - Stack output viewing and installation cleanup - Resource detail views and change set functionality - Dry run fixes and icon updates - Related resources API and change set commands - Stack event fetching and authentication updates - Drift indication and change set deletion - Credential encryption and diff viewer improvements - CFN init UI and empty node set handling - Stack overview pages and environment selection - View diff improvements and resource command updates - CloudFormation prefix usage and change set logic - Command execution controls and deployment confirmation - Document management and resource node simplification - Region management and settings updates - Resource/stack pagination and explorer node additions - Version selection and color fixes - Stack refresh listeners and LSP server locator improvements - CFN-lint/guard settings and diff provider enhancements - Managed resource import warnings and resource import support - Telemetry/client ID configuration and regionality improvements - Clone/stack info restoration and package.json updates - LSP stream improvements and authentication fixes ## Problem ## Solution --- - 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: manodnyab <[email protected]>
1 parent 1225595 commit 371d23b

File tree

64 files changed

+2833
-805
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

64 files changed

+2833
-805
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
packages/core/src/codewhisperer/ @aws/codewhisperer-team
33
packages/core/src/amazonqFeatureDev/ @aws/earlybird
44
packages/core/src/awsService/accessanalyzer/ @aws/access-analyzer
5+
packages/core/src/awsService/cloudformation/ @aws/cfn-dev-productivity
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
export const ResourceIdentifierDocumentationUrl =
7+
'https://docs.aws.amazon.com/cloudcontrolapi/latest/userguide/resource-identifier.html'

packages/core/src/awsService/cloudformation/cfn-init/cfnEnvironmentManager.ts

Lines changed: 26 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
DeploymentConfig,
1414
CfnEnvironmentFileSelectorItem as DeploymentFileDetail,
1515
CfnEnvironmentFileSelectorItem,
16+
unselectedValue,
1617
} from './cfnProjectTypes'
1718
import path from 'path'
1819
import fs from '../../../shared/fs/fs'
@@ -26,12 +27,15 @@ import { DocumentInfo } from './cfnEnvironmentRequestType'
2627
import { parseCfnEnvironmentFiles } from './cfnEnvironmentApi'
2728
import { LanguageClient } from 'vscode-languageclient/node'
2829
import { Parameter } from '@aws-sdk/client-cloudformation'
29-
import { convertRecordToParameters, convertRecordToTags } from './utils'
30+
import {
31+
convertRecordToParameters,
32+
convertRecordToTags,
33+
getConfigPath,
34+
getEnvironmentDir,
35+
getProjectDir,
36+
} from './utils'
3037

3138
export class CfnEnvironmentManager implements Disposable {
32-
private readonly cfnProjectPath = 'cfn-project'
33-
private readonly configFile = 'cfn-config.json'
34-
private readonly environmentsDirectory = 'environments'
3539
private readonly selectedEnvironmentKey = 'aws.cloudformation.selectedEnvironment'
3640
private readonly auth = Auth.instance
3741
private listeners: (() => void)[] = []
@@ -98,8 +102,8 @@ export class CfnEnvironmentManager implements Disposable {
98102
}
99103

100104
private async isProjectInitialized(): Promise<boolean> {
101-
const configPath = await this.getConfigPath()
102-
const projectDirectory = await this.getProjectDir()
105+
const configPath = await getConfigPath()
106+
const projectDirectory = await getProjectDir()
103107

104108
return (await fs.existsFile(configPath)) && (await fs.existsDir(projectDirectory))
105109
}
@@ -114,6 +118,8 @@ export class CfnEnvironmentManager implements Disposable {
114118
await globals.context.workspaceState.update(this.selectedEnvironmentKey, environmentName)
115119

116120
await this.syncEnvironmentWithProfile(environment)
121+
} else {
122+
await globals.context.workspaceState.update(this.selectedEnvironmentKey, undefined)
117123
}
118124

119125
this.notifyListeners()
@@ -133,7 +139,7 @@ export class CfnEnvironmentManager implements Disposable {
133139
}
134140

135141
public async fetchAvailableEnvironments(): Promise<CfnEnvironmentLookup> {
136-
const configPath = await this.getConfigPath()
142+
const configPath = await getConfigPath()
137143
const config = JSON.parse(await fs.readFileText(configPath)) as CfnConfig
138144

139145
return config.environments
@@ -151,7 +157,7 @@ export class CfnEnvironmentManager implements Disposable {
151157
}
152158

153159
try {
154-
const environmentDir = await this.getEnvironmentDir(environmentName)
160+
const environmentDir = await getEnvironmentDir(environmentName)
155161
const files = await fs.readdir(environmentDir)
156162

157163
const filesToParse: DocumentInfo[] = await Promise.all(
@@ -194,6 +200,18 @@ export class CfnEnvironmentManager implements Disposable {
194200
return await this.environmentFileSelector.selectEnvironmentFile(selectorItems, requiredParameters.length)
195201
}
196202

203+
public async refreshSelectedEnvironment() {
204+
const environmentName = this.getSelectedEnvironmentName()
205+
const availableEnvironments = await this.fetchAvailableEnvironments()
206+
207+
// unselect environment if an environment was manually deleted
208+
if (environmentName && !availableEnvironments[environmentName]) {
209+
await this.setSelectedEnvironment(unselectedValue, availableEnvironments)
210+
211+
return undefined
212+
}
213+
}
214+
197215
private async createEnvironmentFileSelectorItem(
198216
fileName: string,
199217
deploymentConfig: DeploymentConfig,
@@ -243,30 +261,6 @@ export class CfnEnvironmentManager implements Disposable {
243261
}
244262
}
245263

246-
public async getEnvironmentDir(environmentName: string): Promise<string> {
247-
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
248-
if (!workspaceRoot) {
249-
throw new Error('No workspace folder found')
250-
}
251-
return path.join(workspaceRoot, this.cfnProjectPath, this.environmentsDirectory, environmentName)
252-
}
253-
254-
private async getConfigPath(): Promise<string> {
255-
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
256-
if (!workspaceRoot) {
257-
throw new Error('No workspace folder found')
258-
}
259-
return path.join(workspaceRoot, this.cfnProjectPath, this.configFile)
260-
}
261-
262-
private async getProjectDir(): Promise<string> {
263-
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
264-
if (!workspaceRoot) {
265-
throw new Error('No workspace folder found')
266-
}
267-
return path.join(workspaceRoot, this.cfnProjectPath)
268-
}
269-
270264
dispose(): void {
271265
// No resources to dispose
272266
}

packages/core/src/awsService/cloudformation/cfn-init/cfnInitCliCaller.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import * as path from 'path'
77
import * as vscode from 'vscode'
88
import { ChildProcess } from '../../../shared/utilities/processUtils'
9+
import { extractErrorMessage } from '../utils'
910

1011
export interface EnvironmentOption {
1112
name: string
@@ -63,10 +64,16 @@ export class CfnInitCliCaller {
6364
},
6465
})
6566

66-
return result.exitCode === 0
67-
? { success: true, output: result.stdout || undefined }
68-
: { success: false, error: result.stderr || `Process exited with code ${result.exitCode}` }
67+
if (result.exitCode === 0) {
68+
return { success: true, output: result.stdout || undefined }
69+
} else {
70+
void vscode.window.showWarningMessage(
71+
`cfn init command returned exit code ${result.exitCode}: ${result.stderr} - ${result.stdout} - ${extractErrorMessage(result.error)}`
72+
)
73+
return { success: false, error: result.stderr || `Process exited with code ${result.exitCode}` }
74+
}
6975
} catch (error) {
76+
void vscode.window.showErrorMessage(`Error executing cfn init command: ${extractErrorMessage(error)}`)
7077
return { success: false, error: error instanceof Error ? error.message : String(error) }
7178
}
7279
}

packages/core/src/awsService/cloudformation/cfn-init/cfnInitUiInterface.ts

Lines changed: 68 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import { CfnInitCliCaller, EnvironmentOption } from './cfnInitCliCaller'
88
import { Auth } from '../../../auth/auth'
99
import { promptForConnection } from '../../../auth/utils'
1010
import { getEnvironmentName, getProjectName, getProjectPath } from '../ui/inputBox'
11+
import fs from '../../../shared/fs/fs'
12+
import path from 'path'
13+
import { unselectedValue } from './cfnProjectTypes'
1114

1215
interface FormState {
1316
projectName?: string
@@ -22,8 +25,22 @@ export class CfnInitUiInterface {
2225

2326
async promptForCreate() {
2427
try {
25-
// Set default project path
26-
this.state.projectPath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || process.cwd()
28+
// Set default project path with validation
29+
const defaultPath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || process.cwd()
30+
31+
// Validate default path
32+
try {
33+
await fs.checkPerms(defaultPath, '*w*')
34+
const cfnProjectPath = path.join(defaultPath, 'cfn-project')
35+
const cfnProjectExists = await fs.existsDir(cfnProjectPath)
36+
37+
// Only use default if it's valid and doesn't have cfn-project
38+
this.state.projectPath = cfnProjectExists ? undefined : defaultPath
39+
} catch {
40+
// Default path is invalid, leave undefined to force user selection
41+
this.state.projectPath = undefined
42+
}
43+
2744
await this.showForm()
2845
} catch (error) {
2946
void vscode.window.showErrorMessage(`CFN Init failed: ${error}`)
@@ -40,12 +57,12 @@ export class CfnInitUiInterface {
4057
const updateItems = () => {
4158
const items = [
4259
{
43-
label: `${this.state.projectName ? '[✓]' : '[ ]'} Project Name`,
44-
detail: this.state.projectName || 'Click to set project name',
60+
label: `Project Name`,
61+
detail: this.state.projectName || unselectedValue,
4562
},
4663
{
47-
label: `${this.state.projectPath ? '[✓]' : '[ ]'} Project Path`,
48-
detail: this.state.projectPath || 'Click to set project path',
64+
label: `Project Path`,
65+
detail: this.state.projectPath || unselectedValue,
4966
},
5067
]
5168

@@ -57,10 +74,11 @@ export class CfnInitUiInterface {
5774
})
5875
}
5976

60-
items.push({
77+
const addEnvItem = {
6178
label: '$(plus) Add Environment (At least one required)',
6279
detail: 'Configure a new deployment environment',
63-
})
80+
}
81+
items.push(addEnvItem)
6482

6583
if (this.state.environments.length > 0) {
6684
items.push({
@@ -69,7 +87,24 @@ export class CfnInitUiInterface {
6987
})
7088
}
7189

90+
const createProjectItem = {
91+
label: '$(check) Create Project',
92+
detail: 'Create the CloudFormation project with current configuration',
93+
}
94+
items.push(createProjectItem)
95+
7296
quickPick.items = items
97+
98+
// Highlight first undefined state property
99+
if (!this.state.projectName) {
100+
quickPick.activeItems = [items[0]]
101+
} else if (!this.state.projectPath) {
102+
quickPick.activeItems = [items[1]]
103+
} else if (this.state.environments.length === 0) {
104+
quickPick.activeItems = [addEnvItem]
105+
} else {
106+
quickPick.activeItems = [createProjectItem]
107+
}
73108
}
74109

75110
updateItems()
@@ -98,23 +133,21 @@ export class CfnInitUiInterface {
98133
await this.addEnvironment()
99134
} else if (selected.label.includes('Delete Environment')) {
100135
await this.deleteEnvironment()
136+
} else if (selected.label.includes('Create Project')) {
137+
if (await this.isFormStateValid()) {
138+
quickPick.hide()
139+
resolve(true)
140+
await this.executeProject()
141+
}
142+
return
101143
}
102144

103145
updateItems()
104146
quickPick.show()
105147
})
106148

107149
quickPick.onDidTriggerButton(async () => {
108-
if (!this.state.projectName) {
109-
void vscode.window.showWarningMessage('Project name is required')
110-
return
111-
}
112-
if (!this.state.projectPath) {
113-
void vscode.window.showWarningMessage('Project path is required')
114-
return
115-
}
116-
if (this.state.environments.length === 0) {
117-
void vscode.window.showWarningMessage('At least one environment is required')
150+
if (!(await this.isFormStateValid())) {
118151
return
119152
}
120153
quickPick.hide()
@@ -127,6 +160,23 @@ export class CfnInitUiInterface {
127160
})
128161
}
129162

163+
private async isFormStateValid(): Promise<boolean> {
164+
if (!this.state.projectName) {
165+
void vscode.window.showWarningMessage('Project name is required')
166+
return false
167+
}
168+
if (!this.state.projectPath) {
169+
void vscode.window.showWarningMessage('Project path is required')
170+
return false
171+
}
172+
if (this.state.environments.length === 0) {
173+
void vscode.window.showWarningMessage('At least one environment is required')
174+
return false
175+
}
176+
177+
return true
178+
}
179+
130180
async collectEnvironmentConfig(): Promise<EnvironmentOption | undefined> {
131181
const envName = await getEnvironmentName()
132182

packages/core/src/awsService/cloudformation/cfn-init/cfnProjectTypes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,5 @@ export type CfnEnvironmentFileSelectorItem = {
3737
compatibleParameters?: Parameter[]
3838
optionalFlags?: ChangeSetOptionalFlags
3939
}
40+
41+
export const unselectedValue = '-'

packages/core/src/awsService/cloudformation/cfn-init/utils.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44
*/
55

66
import { Parameter, Tag } from '@aws-sdk/client-cloudformation'
7+
import path from 'path'
8+
import { workspace } from 'vscode'
9+
10+
const cfnProjectPath = 'cfn-project'
11+
const configFile = 'cfn-config.json'
12+
const environmentsDirectory = 'environments'
713

814
export function convertRecordToParameters(parameters: Record<string, string>): Parameter[] {
915
return Object.entries(parameters).map(([key, value]) => ({
@@ -30,3 +36,27 @@ export function convertParametersToRecord(parameters: Parameter[]): Record<strin
3036
export function convertTagsToRecord(tags: Tag[]): Record<string, string> {
3137
return Object.fromEntries(tags.filter((tag) => tag.Key && tag.Value).map((tag) => [tag.Key!, tag.Value!]))
3238
}
39+
40+
export async function getEnvironmentDir(environmentName: string): Promise<string> {
41+
const workspaceRoot = getWorkspaceRoot()
42+
return path.join(workspaceRoot, cfnProjectPath, environmentsDirectory, environmentName)
43+
}
44+
45+
export async function getConfigPath(): Promise<string> {
46+
const workspaceRoot = getWorkspaceRoot()
47+
return path.join(workspaceRoot, cfnProjectPath, configFile)
48+
}
49+
50+
export async function getProjectDir(): Promise<string> {
51+
const workspaceRoot = getWorkspaceRoot()
52+
return path.join(workspaceRoot, cfnProjectPath)
53+
}
54+
55+
export function getWorkspaceRoot(): string {
56+
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
57+
if (!workspaceRoot) {
58+
throw new Error('You must open a workspace to use CFN environment commands')
59+
}
60+
61+
return workspaceRoot
62+
}

0 commit comments

Comments
 (0)