Skip to content

Commit 9c7593c

Browse files
author
Nathan Turinski
committed
Clean up some of the code
1 parent a331ddc commit 9c7593c

File tree

8 files changed

+673
-177
lines changed

8 files changed

+673
-177
lines changed

extension.bundle.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export * from './src/utils/durableUtils';
3333
export { activateInternal, deactivateInternal } from './src/extension';
3434
export * from './src/extensionVariables';
3535
export * from './src/funcConfig/function';
36-
export { extractFuncHostErrorContextForErrorMessage, isFuncHostErrorLog } from './src/funcCoreTools/funcHostErrorUtils';
36+
export { addErrorLinesFromChunk, isFuncHostErrorLog } from './src/funcCoreTools/funcHostErrorUtils';
3737
export * from './src/funcCoreTools/hasMinFuncCliVersion';
3838
export * from './src/FuncVersion';
3939
export * from './src/templates/CentralTemplateProvider';
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { type Site, type WebSiteManagementClient } from '@azure/arm-appservice';
7+
import { LocationListStep } from '@microsoft/vscode-azext-azureutils';
8+
import { AzureWizardExecuteStepWithActivityOutput, nonNullProp } from '@microsoft/vscode-azext-utils';
9+
import { type AppResource } from '@microsoft/vscode-azext-utils/hostapi';
10+
import * as fs from 'fs';
11+
import * as os from 'os';
12+
import * as path from 'path';
13+
import { type Progress, Uri, ViewColumn, window, workspace } from 'vscode';
14+
import { webProvider } from '../../../constants';
15+
import { ext } from '../../../extensionVariables';
16+
import { localize } from '../../../localize';
17+
import { createWebSiteClient } from '../../../utils/azureClients';
18+
import { cpUtils } from '../../../utils/cpUtils';
19+
import { type IFlexFunctionAppWizardContext, type IFunctionAppWizardContext } from '../IFunctionAppWizardContext';
20+
import { generateAzdYaml } from './generateAzdYaml';
21+
import { generateBicepTemplate } from './generateBicepTemplate';
22+
23+
/**
24+
* An execute step that:
25+
* 1. Generates a Bicep template from the wizard context
26+
* 2. Writes it (along with azure.yaml) to a temporary directory
27+
* 3. Runs `azd provision` to deploy the infrastructure
28+
* 4. Retrieves the created Function App Site object from ARM and sets it on context
29+
*
30+
* This replaces the individual ResourceGroupCreateStep, StorageAccountCreateStep,
31+
* AppServicePlanCreateStep, LogAnalyticsCreateStep, AppInsightsCreateStep,
32+
* FunctionAppCreateStep, UserAssignedIdentityCreateStep, and RoleAssignmentExecuteStep
33+
* for the standard (non-Docker) flow.
34+
*/
35+
export class AzdProvisionExecuteStep extends AzureWizardExecuteStepWithActivityOutput<IFunctionAppWizardContext> {
36+
// This step replaces ALL individual ARM create steps, so it runs at the end
37+
public priority: number = 50;
38+
public stepName: string = 'azdProvisionStep';
39+
40+
public async execute(context: IFlexFunctionAppWizardContext, progress: Progress<{ message?: string; increment?: number }>): Promise<void> {
41+
const siteName = nonNullProp(context, 'newSiteName');
42+
const rgName = nonNullProp(context, 'newResourceGroupName');
43+
const location = await LocationListStep.getLocation(context, webProvider);
44+
const locationName = nonNullProp(location, 'name');
45+
46+
// 1. Generate the Bicep template and azure.yaml from wizard context
47+
progress.report({ message: localize('generatingInfra', 'Generating infrastructure template...') });
48+
const { bicepContent, resourcesBicepContent, parametersContent } = generateBicepTemplate(context);
49+
const azdYaml = generateAzdYaml(context);
50+
51+
// 2. Write to a temporary AZD project directory
52+
const tmpDir = await this.createTempAzdProject(siteName, bicepContent, resourcesBicepContent, parametersContent, azdYaml);
53+
54+
try {
55+
// 2b. Open the generated Bicep file in the editor so the user can see the template
56+
const mainBicepPath = path.join(tmpDir, 'infra', 'main.bicep');
57+
const doc = await workspace.openTextDocument(Uri.file(mainBicepPath));
58+
await window.showTextDocument(doc, { viewColumn: ViewColumn.Beside, preview: true, preserveFocus: true });
59+
60+
// 3. Set up the AZD environment by writing files directly (avoids shell quoting issues)
61+
progress.report({ message: localize('initAzdEnv', 'Initializing AZD environment...') });
62+
const envName = sanitizeEnvName(siteName);
63+
64+
await this.writeAzdEnvironment(tmpDir, envName, {
65+
AZURE_LOCATION: locationName,
66+
AZURE_SUBSCRIPTION_ID: context.subscriptionId,
67+
});
68+
69+
// 4. Run azd provision
70+
progress.report({ message: localize('provisioning', 'Provisioning resources with AZD for "{0}"...', siteName) });
71+
72+
await cpUtils.executeCommand(
73+
ext.outputChannel,
74+
tmpDir,
75+
'azd',
76+
['provision', '--no-prompt'],
77+
);
78+
79+
// 5. Retrieve the created Function App from ARM to populate context.site
80+
progress.report({ message: localize('retrievingSite', 'Retrieving created function app...') });
81+
const client: WebSiteManagementClient = await createWebSiteClient(context);
82+
const site: Site = await client.webApps.get(rgName, siteName);
83+
84+
context.site = site;
85+
context.activityResult = site as AppResource;
86+
87+
ext.outputChannel.appendLog(localize('azdProvisionSuccess', 'Successfully provisioned function app "{0}" via AZD.', siteName));
88+
} finally {
89+
// 6. Clean up the temp directory
90+
this.cleanupTempDir(tmpDir);
91+
}
92+
}
93+
94+
public shouldExecute(context: IFunctionAppWizardContext): boolean {
95+
return !context.site;
96+
}
97+
98+
/**
99+
* Creates a temporary directory with the AZD project structure:
100+
* tmpDir/
101+
* azure.yaml
102+
* infra/
103+
* main.bicep
104+
* resources.bicep
105+
* main.parameters.json
106+
*/
107+
private async createTempAzdProject(
108+
siteName: string,
109+
bicepContent: string,
110+
resourcesBicepContent: string,
111+
parametersContent: string,
112+
azdYaml: string,
113+
): Promise<string> {
114+
const tmpDir = path.join(os.tmpdir(), `azfunc-azd-${siteName}-${Date.now()}`);
115+
const infraDir = path.join(tmpDir, 'infra');
116+
117+
await fs.promises.mkdir(infraDir, { recursive: true });
118+
await Promise.all([
119+
fs.promises.writeFile(path.join(tmpDir, 'azure.yaml'), azdYaml, 'utf-8'),
120+
fs.promises.writeFile(path.join(infraDir, 'main.bicep'), bicepContent, 'utf-8'),
121+
fs.promises.writeFile(path.join(infraDir, 'resources.bicep'), resourcesBicepContent, 'utf-8'),
122+
fs.promises.writeFile(path.join(infraDir, 'main.parameters.json'), parametersContent, 'utf-8'),
123+
]);
124+
125+
return tmpDir;
126+
}
127+
128+
/**
129+
* Writes AZD environment config files directly, avoiding shell quoting issues
130+
* with `azd env set`. Creates:
131+
* .azure/config.json — sets default environment
132+
* .azure/<envName>/.env — environment variable values
133+
*/
134+
private async writeAzdEnvironment(
135+
projectDir: string,
136+
envName: string,
137+
envVars: Record<string, string>,
138+
): Promise<void> {
139+
const azureDir = path.join(projectDir, '.azure');
140+
const envDir = path.join(azureDir, envName);
141+
142+
await fs.promises.mkdir(envDir, { recursive: true });
143+
144+
// Write .azure/config.json to set the default environment
145+
const configJson = JSON.stringify({ version: 1, defaultEnvironment: envName }, null, 2);
146+
await fs.promises.writeFile(path.join(azureDir, 'config.json'), configJson, 'utf-8');
147+
148+
// Write .azure/<envName>/.env with all environment variables
149+
const envFileContent = Object.entries(envVars)
150+
.map(([key, value]) => `${key}="${value}"`)
151+
.join('\n');
152+
await fs.promises.writeFile(path.join(envDir, '.env'), envFileContent, 'utf-8');
153+
}
154+
155+
/**
156+
* Best-effort cleanup of the temporary AZD project directory.
157+
*/
158+
private cleanupTempDir(tmpDir: string): void {
159+
try {
160+
fs.rmSync(tmpDir, { recursive: true, force: true });
161+
} catch {
162+
// Ignore cleanup errors — the OS will clean temp eventually
163+
}
164+
}
165+
166+
protected getTreeItemLabel(context: IFunctionAppWizardContext): string {
167+
return localize('provisionWithAzd', 'Provision function app "{0}" via AZD', nonNullProp(context, 'newSiteName'));
168+
}
169+
170+
protected getOutputLogSuccess(context: IFunctionAppWizardContext): string {
171+
return localize('azdProvisionSuccess', 'Successfully provisioned function app "{0}" via AZD.', nonNullProp(context, 'newSiteName'));
172+
}
173+
174+
protected getOutputLogFail(context: IFunctionAppWizardContext): string {
175+
return localize('azdProvisionFail', 'Failed to provision function app "{0}" via AZD.', nonNullProp(context, 'newSiteName'));
176+
}
177+
178+
protected getOutputLogProgress(context: IFunctionAppWizardContext): string {
179+
return localize('azdProvisionProgress', 'Provisioning function app "{0}" via AZD...', nonNullProp(context, 'newSiteName'));
180+
}
181+
}
182+
183+
/**
184+
* Sanitizes a Function App name into a valid AZD environment name.
185+
* AZD env names must be alphanumeric with hyphens, 1-64 chars.
186+
*/
187+
function sanitizeEnvName(name: string): string {
188+
return name.replace(/[^a-zA-Z0-9-]/g, '-').substring(0, 64);
189+
}

0 commit comments

Comments
 (0)