Skip to content

Commit 7793c48

Browse files
Merge master into feature/LSP-alpha
2 parents dff2c08 + c8809c6 commit 7793c48

File tree

126 files changed

+16125
-231
lines changed

Some content is hidden

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

126 files changed

+16125
-231
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

CONTRIBUTING.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,11 @@ Unlike the user setting overrides, not all of these environment variables have t
527527

528528
- `SSMDOCUMENT_LANGUAGESERVER_PORT`: The port the ssm document language server should start debugging on
529529

530+
#### CloudFormation LSP
531+
532+
- `__CLOUDFORMATIONLSP_PATH`: for aws.dev.cloudformationLsp.path
533+
- `__CLOUDFORMATIONLSP_CLOUDFORMATION_ENDPOINT`: for aws.dev.cloudformationLsp.cloudformationEndpoint
534+
530535
#### CI/Testing
531536

532537
- `GITHUB_ACTION`: The name of the current GitHub Action workflow step that is running
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'
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { Disposable } from 'vscode'
7+
import { LanguageClient } from 'vscode-languageclient/node'
8+
import { StacksManager } from '../stacks/stacksManager'
9+
import { ResourcesManager } from '../resources/resourcesManager'
10+
import { CloudFormationRegionManager } from '../explorer/regionManager'
11+
import globals from '../../../shared/extensionGlobals'
12+
import * as jose from 'jose'
13+
import * as crypto from 'crypto'
14+
15+
export const encryptionKey = crypto.randomBytes(32)
16+
17+
export class AwsCredentialsService implements Disposable {
18+
private authChangeListener: Disposable
19+
private client: LanguageClient | undefined
20+
21+
constructor(
22+
private stacksManager: StacksManager,
23+
private resourcesManager: ResourcesManager,
24+
private regionManager: CloudFormationRegionManager
25+
) {
26+
this.authChangeListener = globals.awsContext.onDidChangeContext(() => {
27+
void this.updateCredentialsFromActiveConnection()
28+
})
29+
}
30+
31+
async initialize(client: LanguageClient): Promise<void> {
32+
this.client = client
33+
await this.updateCredentialsFromActiveConnection()
34+
}
35+
36+
private async updateCredentialsFromActiveConnection(): Promise<void> {
37+
if (!this.client) {
38+
return
39+
}
40+
41+
const credentials = await globals.awsContext.getCredentials()
42+
const profileName = globals.awsContext.getCredentialProfileName()
43+
44+
if (credentials && profileName) {
45+
const encryptedRequest = await this.createEncryptedCredentialsRequest({
46+
profile: profileName.replaceAll('profile:', ''),
47+
region: this.regionManager.getSelectedRegion(),
48+
accessKeyId: credentials.accessKeyId,
49+
secretAccessKey: credentials.secretAccessKey,
50+
sessionToken: credentials.sessionToken,
51+
})
52+
53+
await this.client.sendRequest('aws/credentials/iam/update', encryptedRequest)
54+
}
55+
56+
void this.stacksManager.reload()
57+
void this.resourcesManager.reload()
58+
}
59+
60+
async updateRegion(): Promise<void> {
61+
await this.updateCredentialsFromActiveConnection()
62+
}
63+
64+
private async createEncryptedCredentialsRequest(data: any): Promise<any> {
65+
const payload = new TextEncoder().encode(JSON.stringify({ data }))
66+
67+
const jwt = await new jose.CompactEncrypt(payload)
68+
.setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })
69+
.encrypt(encryptionKey)
70+
71+
return {
72+
data: jwt,
73+
encrypted: true,
74+
}
75+
}
76+
77+
dispose(): void {
78+
this.authChangeListener.dispose()
79+
}
80+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { LanguageClient } from 'vscode-languageclient/node'
7+
import {
8+
ParsedCfnEnvironmentFile,
9+
ParseCfnEnvironmentFilesParams,
10+
ParseCfnEnvironmentFilesRequest,
11+
} from './cfnEnvironmentRequestType'
12+
13+
export async function parseCfnEnvironmentFiles(
14+
client: LanguageClient,
15+
params: ParseCfnEnvironmentFilesParams
16+
): Promise<ParsedCfnEnvironmentFile[]> {
17+
const result = await client.sendRequest(ParseCfnEnvironmentFilesRequest, params)
18+
return result.parsedFiles
19+
}
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { Disposable, Uri, window, workspace, commands } from 'vscode'
7+
import { Auth } from '../../../auth/auth'
8+
import { commandKey, extractErrorMessage, formatMessage, toString } from '../utils'
9+
import {
10+
CfnConfig,
11+
CfnEnvironmentConfig,
12+
CfnEnvironmentLookup,
13+
DeploymentConfig,
14+
CfnEnvironmentFileSelectorItem as DeploymentFileDetail,
15+
CfnEnvironmentFileSelectorItem,
16+
unselectedValue,
17+
} from './cfnProjectTypes'
18+
import path from 'path'
19+
import fs from '../../../shared/fs/fs'
20+
import { CfnEnvironmentSelector } from '../ui/cfnEnvironmentSelector'
21+
import { CfnEnvironmentFileSelector } from '../ui/cfnEnvironmentFileSelector'
22+
import globals from '../../../shared/extensionGlobals'
23+
import { TemplateParameter } from '../stacks/actions/stackActionRequestType'
24+
import { validateParameterValue } from '../stacks/actions/stackActionInputValidation'
25+
import { getLogger } from '../../../shared/logger/logger'
26+
import { DocumentInfo } from './cfnEnvironmentRequestType'
27+
import { parseCfnEnvironmentFiles } from './cfnEnvironmentApi'
28+
import { LanguageClient } from 'vscode-languageclient/node'
29+
import { Parameter } from '@aws-sdk/client-cloudformation'
30+
import {
31+
convertRecordToParameters,
32+
convertRecordToTags,
33+
getConfigPath,
34+
getEnvironmentDir,
35+
getProjectDir,
36+
} from './utils'
37+
38+
export class CfnEnvironmentManager implements Disposable {
39+
private readonly selectedEnvironmentKey = 'aws.cloudformation.selectedEnvironment'
40+
private readonly auth = Auth.instance
41+
private listeners: (() => void)[] = []
42+
43+
private readonly initializeOption = 'Initialize Project'
44+
45+
constructor(
46+
private readonly client: LanguageClient,
47+
private readonly environmentSelector: CfnEnvironmentSelector,
48+
private readonly environmentFileSelector: CfnEnvironmentFileSelector
49+
) {}
50+
51+
public addListener(listener: () => void): void {
52+
this.listeners.push(listener)
53+
}
54+
55+
public getSelectedEnvironmentName(): string | undefined {
56+
return globals.context.workspaceState.get(this.selectedEnvironmentKey)
57+
}
58+
59+
private notifyListeners(): void {
60+
for (const listener of this.listeners) {
61+
listener()
62+
}
63+
}
64+
65+
public async promptInitializeIfNeeded(operation: string): Promise<boolean> {
66+
if (!(await this.isProjectInitialized())) {
67+
const choice = await window.showWarningMessage(
68+
`You must initialize your CFN Project to perform ${operation}`,
69+
this.initializeOption
70+
)
71+
72+
if (choice === this.initializeOption) {
73+
void commands.executeCommand(commandKey('init.initializeProject'))
74+
}
75+
return true
76+
}
77+
78+
return false
79+
}
80+
81+
public async selectEnvironment(): Promise<void> {
82+
if (await this.promptInitializeIfNeeded('Environment Selection')) {
83+
return
84+
}
85+
86+
let environmentLookup: CfnEnvironmentLookup
87+
88+
try {
89+
environmentLookup = await this.fetchAvailableEnvironments()
90+
} catch (error) {
91+
void window.showErrorMessage(
92+
formatMessage(`Failed to retrieve environments from configuration: ${toString(error)}`)
93+
)
94+
return
95+
}
96+
97+
const environmentName = await this.environmentSelector.selectEnvironment(environmentLookup)
98+
99+
if (environmentName) {
100+
await this.setSelectedEnvironment(environmentName, environmentLookup)
101+
}
102+
}
103+
104+
private async isProjectInitialized(): Promise<boolean> {
105+
const configPath = await getConfigPath()
106+
const projectDirectory = await getProjectDir()
107+
108+
return (await fs.existsFile(configPath)) && (await fs.existsDir(projectDirectory))
109+
}
110+
111+
private async setSelectedEnvironment(
112+
environmentName: string,
113+
environmentLookup: CfnEnvironmentLookup
114+
): Promise<void> {
115+
const environment = environmentLookup[environmentName]
116+
117+
if (environment) {
118+
await globals.context.workspaceState.update(this.selectedEnvironmentKey, environmentName)
119+
120+
await this.syncEnvironmentWithProfile(environment)
121+
} else {
122+
await globals.context.workspaceState.update(this.selectedEnvironmentKey, undefined)
123+
}
124+
125+
this.notifyListeners()
126+
}
127+
128+
private async syncEnvironmentWithProfile(environment: CfnEnvironmentConfig) {
129+
const profileName = environment.profile
130+
131+
const currentConnection = await this.auth.getConnection({ id: `profile:${profileName}` })
132+
133+
if (!currentConnection) {
134+
void window.showErrorMessage(formatMessage(`No connection found for profile: ${profileName}`))
135+
return
136+
}
137+
138+
await this.auth.useConnection(currentConnection)
139+
}
140+
141+
public async fetchAvailableEnvironments(): Promise<CfnEnvironmentLookup> {
142+
const configPath = await getConfigPath()
143+
const config = JSON.parse(await fs.readFileText(configPath)) as CfnConfig
144+
145+
return config.environments
146+
}
147+
148+
public async selectEnvironmentFile(
149+
templateUri: string,
150+
requiredParameters: TemplateParameter[]
151+
): Promise<CfnEnvironmentFileSelectorItem | undefined> {
152+
const environmentName = this.getSelectedEnvironmentName()
153+
const selectorItems: CfnEnvironmentFileSelectorItem[] = []
154+
155+
if (!environmentName) {
156+
return undefined
157+
}
158+
159+
try {
160+
const environmentDir = await getEnvironmentDir(environmentName)
161+
const files = await fs.readdir(environmentDir)
162+
163+
const filesToParse: DocumentInfo[] = await Promise.all(
164+
files
165+
.filter(
166+
([fileName]) =>
167+
fileName.endsWith('.json') || fileName.endsWith('.yaml') || fileName.endsWith('.yml')
168+
)
169+
.map(async ([fileName]) => {
170+
const filePath = path.join(environmentDir, fileName)
171+
const content = await fs.readFileText(filePath)
172+
const type = fileName.endsWith('.json') ? 'JSON' : 'YAML'
173+
174+
return {
175+
type,
176+
content,
177+
fileName,
178+
}
179+
})
180+
)
181+
182+
const environmentFiles = await parseCfnEnvironmentFiles(this.client, { documents: filesToParse })
183+
184+
for (const deploymentFile of environmentFiles) {
185+
const item = await this.createEnvironmentFileSelectorItem(
186+
deploymentFile.fileName,
187+
deploymentFile.deploymentConfig,
188+
requiredParameters,
189+
templateUri
190+
)
191+
if (item) {
192+
selectorItems.push(item)
193+
}
194+
}
195+
} catch (error) {
196+
void window.showErrorMessage(`Error loading deployment files: ${extractErrorMessage(error)}`)
197+
return undefined
198+
}
199+
200+
return await this.environmentFileSelector.selectEnvironmentFile(selectorItems, requiredParameters.length)
201+
}
202+
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+
215+
private async createEnvironmentFileSelectorItem(
216+
fileName: string,
217+
deploymentConfig: DeploymentConfig,
218+
requiredParameters: TemplateParameter[],
219+
templateUri: string
220+
): Promise<DeploymentFileDetail | undefined> {
221+
try {
222+
return {
223+
fileName: fileName,
224+
hasMatchingTemplatePath:
225+
workspace.asRelativePath(Uri.parse(templateUri)) === deploymentConfig.templateFilePath,
226+
compatibleParameters: this.getCompatibleParams(deploymentConfig, requiredParameters),
227+
optionalFlags: {
228+
tags: deploymentConfig.tags ? convertRecordToTags(deploymentConfig.tags) : undefined,
229+
includeNestedStacks: deploymentConfig.includeNestedStacks,
230+
importExistingResources: deploymentConfig.importExistingResources,
231+
onStackFailure: deploymentConfig.onStackFailure,
232+
},
233+
}
234+
} catch (error) {
235+
getLogger().warn(`Failed to create selector item ${fileName}:`, error)
236+
}
237+
}
238+
239+
private getCompatibleParams(
240+
deploymentConfig: DeploymentConfig,
241+
requiredParameters: TemplateParameter[]
242+
): Parameter[] | undefined {
243+
if (deploymentConfig.parameters && requiredParameters.length > 0) {
244+
const parameters = deploymentConfig.parameters
245+
246+
// Filter only parameters that are in template and are valid
247+
const validParams = requiredParameters.filter((templateParam) => {
248+
if (!(templateParam.name in parameters)) {
249+
return false
250+
}
251+
const value = parameters[templateParam.name]
252+
return validateParameterValue(value, templateParam) === undefined
253+
})
254+
255+
const validParameterNames = validParams.map((p) => p.name)
256+
const filteredParameters = Object.fromEntries(
257+
Object.entries(parameters).filter(([key]) => validParameterNames.includes(key))
258+
)
259+
260+
return convertRecordToParameters(filteredParameters)
261+
}
262+
}
263+
264+
dispose(): void {
265+
// No resources to dispose
266+
}
267+
}

0 commit comments

Comments
 (0)