Skip to content
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

Improve local settings when creating templates #3924

Merged
merged 8 commits into from
Dec 15, 2023
Merged
Show file tree
Hide file tree
Changes from 6 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
4 changes: 2 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true,
"source.organizeImports": true
"source.fixAll.eslint": "explicit",
"source.organizeImports": "explicit"
},
"editor.detectIndentation": false,
"editor.formatOnSave": true,
Expand Down
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 0 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -276,11 +276,6 @@
"title": "%azureFunctions.restartFunctionApp%",
"category": "Azure Functions"
},
{
"command": "azureFunctions.setAzureWebJobsStorage",
Copy link
Contributor

Choose a reason for hiding this comment

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

Also setAzureWebJobsStorage is never actually invoked anywhere, so I went ahead and deleted that.

I use it a decent amount please don't remove it 🥺 If you clone a function app repo or just accidentally delete your "local.settings.json" it's a fast way to get the file back

Copy link
Member Author

Choose a reason for hiding this comment

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

Hahaha okay. I figured the only entry point was through the command palette, so who could even be using it...? But I guess you are 1 of 85 people per month.

image

"title": "%azureFunctions.setAzureWebJobsStorage%",
"category": "Azure Functions"
},
{
"command": "azureFunctions.startFunctionApp",
"title": "%azureFunctions.startFunctionApp%",
Expand Down
1 change: 0 additions & 1 deletion package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@
"azureFunctions.requestTimeout": "The timeout (in seconds) to be used when making requests, for example getting the latest templates.",
"azureFunctions.restartFunctionApp": "Restart",
"azureFunctions.scmDoBuildDuringDeployment": "Set to true to build your project on the server. Currently only applicable for Linux Function Apps.",
"azureFunctions.setAzureWebJobsStorage": "Set AzureWebJobsStorage...",
"azureFunctions.show64BitWarning": "Show a warning to install a 64-bit version of the Azure Functions Core Tools when you create a .NET Framework project.",
"azureFunctions.showCoreToolsWarning": "Show a warning if your installed version of Azure Functions Core Tools is out-of-date.",
"azureFunctions.showDeployConfirmation": "Ask for confirmation before deploying to a Function App in Azure (deploying will overwrite any previous deployment and cannot be undone).",
Expand Down
1 change: 1 addition & 0 deletions src/commands/addBinding/IBindingWizardContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ export interface IBindingWizardContext extends IFunctionWizardContext {
bindingTemplate?: IBindingTemplate;
binding?: IFunctionBinding;
bindingName?: string;
useStorageEmulator?: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ export abstract class AzureConnectionCreateStepBase<T extends IBindingWizardCont
progress.report({ message: localize('retrieving', 'Retrieving connection string...') });

const result: IConnection = await this.getConnection(context);
let appSettingKey: string = `${result.name}_${nonNullProp(this._setting, 'resourceType').toUpperCase()}`;
let appSettingKey: string = context.useStorageEmulator ?
// if using the storage emulator, don't append the resource type to the app setting key
Copy link
Member

Choose a reason for hiding this comment

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

nit: The comment explains what we're doing but not why. I think it's more helpful if the why was included.

Copy link
Member Author

Choose a reason for hiding this comment

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

To be honest, I'm not really sure why @ejizba did this in the first place.

https://learn.microsoft.com/en-us/azure/azure-functions/functions-app-settings#azurewebjobsstorage

Here's the documentation about the setting AzureWebJobsStorage which is the default app setting linking to a storage account.

The old behavior would take the name of the azure resource and append the resource type (so like, naturins-storage_STORAGE) as the key. I'm wondering if AzureWebJobsStorage didn't exist at that time so he was just finding a common convention for the key?

Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure if he's talking about what I did or what you did @nturinski. The "_STORAGE" thing was just a random naming convention and I feel like I was probably just following an existing pattern from docs or the portal

In any case, I'm confused about what you're doing just like Alex probably is. Like, what does the emulator have to do with appending "_STORAGE"? Seems like both "AzureWebJobsStorage" and "randomkey_STORAGE" could use or not use the emulator, right?

Copy link
Member Author

Choose a reason for hiding this comment

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

The issue is when I pass in AzureWebJobsStorage as the app key, it'll automatically append _STORAGE to the end of it. You can see it in my screenshot where that happened.

Is it pretty common to have other storage connection strings outside of AzureWebJobsStorage?

result.name :
`${result.name}_${nonNullProp(this._setting, 'resourceType').toUpperCase()}`;
appSettingKey = appSettingKey.replace(/[^a-z0-9_\.]/gi, ''); // remove invalid chars
setBindingSetting(context, this._setting, appSettingKey);
await setLocalAppSetting(context, context.projectPath, appSettingKey, result.connectionString);
Expand Down
16 changes: 6 additions & 10 deletions src/commands/addBinding/settingSteps/LocalAppSettingListStep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { StorageAccountKind, StorageAccountListStep, StorageAccountPerformance, StorageAccountReplication } from '@microsoft/vscode-azext-azureutils';
import { type AzureWizardExecuteStep, type AzureWizardPromptStep, type IAzureQuickPickItem, type ISubscriptionActionContext, type IWizardOptions } from '@microsoft/vscode-azext-utils';
import * as path from 'path';
import { localSettingsFileName } from '../../../constants';
Expand All @@ -20,7 +19,7 @@ import { BindingSettingStepBase } from './BindingSettingStepBase';
import { LocalAppSettingCreateStep } from './LocalAppSettingCreateStep';
import { LocalAppSettingNameStep } from './LocalAppSettingNameStep';
import { LocalAppSettingValueStep } from './LocalAppSettingValueStep';
import { StorageConnectionCreateStep } from './StorageConnectionCreateStep';
import { StorageTypePromptStep } from './StorageTypePromptStep';
import { CosmosDBConnectionCreateStep } from './cosmosDB/CosmosDBConnectionCreateStep';
import { CosmosDBListStep } from './cosmosDB/CosmosDBListStep';
import { EventHubAuthRuleListStep } from './eventHub/EventHubAuthRuleListStep';
Expand All @@ -33,10 +32,11 @@ export class LocalAppSettingListStep extends BindingSettingStepBase {
public async promptCore(context: IBindingWizardContext): Promise<BindingSettingValue> {
const localSettingsPath: string = path.join(context.projectPath, localSettingsFileName);
const settings: ILocalSettingsJson = await getLocalSettingsJson(context, localSettingsPath);
const existingSettings: string[] = settings.Values ? Object.keys(settings.Values) : [];
const existingSettings: [string, string][] = settings.Values ? Object.entries(settings.Values) : [];

let picks: IAzureQuickPickItem<string | undefined>[] = [{ label: localize('newAppSetting', '$(plus) Create new local app setting'), data: undefined }];
picks = picks.concat(existingSettings.map((s: string) => { return { data: s, label: s }; }));
const placeHolder: string = localize('selectAppSetting', 'Select setting from "{0}"', localSettingsFileName);
picks = picks.concat(existingSettings.map((s: [string, string]) => { return { data: s[0], label: s[0], description: s[1] }; }));
Copy link
Contributor

Choose a reason for hiding this comment

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

I added the value to the list of local settings.

The value can be a secret, though. You could add an option or something to show values, but they should be hidden by default just to be safe. Just like how app settings are currently displayed in the tree view

Copy link
Member Author

Choose a reason for hiding this comment

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

image
image

const placeHolder: string = localize('selectAppSetting', 'Select the app setting with your {1} string from "{0}"', localSettingsFileName, this._setting.label);
return (await context.ui.showQuickPick(picks, { placeHolder })).data;
}

Expand All @@ -50,11 +50,7 @@ export class LocalAppSettingListStep extends BindingSettingStepBase {
azureExecuteSteps.push(new CosmosDBConnectionCreateStep(this._setting));
break;
case ResourceType.Storage:
azurePromptSteps.push(new StorageAccountListStep(
{ kind: StorageAccountKind.Storage, performance: StorageAccountPerformance.Standard, replication: StorageAccountReplication.LRS },
{ kind: [StorageAccountKind.BlobStorage], learnMoreLink: 'https://aka.ms/T5o0nf' }
));
azureExecuteSteps.push(new StorageConnectionCreateStep(this._setting));
azurePromptSteps.push(new StorageTypePromptStep(this._setting));
break;
case ResourceType.ServiceBus:
azurePromptSteps.push(new ServiceBusListStep());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import { type IBindingWizardContext } from '../IBindingWizardContext';
import { AzureConnectionCreateStepBase, type IConnection } from './AzureConnectionCreateStepBase';

export class StorageConnectionCreateStep extends AzureConnectionCreateStepBase<IStorageAccountWizardContext & IBindingWizardContext> {
public async getConnection(context: IStorageAccountWizardContext): Promise<IConnection> {
public async getConnection(context: IStorageAccountWizardContext & Partial<IBindingWizardContext>): Promise<IConnection> {
return await getStorageConnectionString(context);
}

public shouldExecute(context: IStorageAccountWizardContext): boolean {
return !!context.storageAccount;
public shouldExecute(context: IStorageAccountWizardContext & Partial<IBindingWizardContext>): boolean {
return !!context.storageAccount || !!context.useStorageEmulator;
}
}
54 changes: 54 additions & 0 deletions src/commands/addBinding/settingSteps/StorageTypePromptStep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { StorageAccountKind, StorageAccountListStep, StorageAccountPerformance, StorageAccountReplication } from "@microsoft/vscode-azext-azureutils";
import { AzureWizardPromptStep, type AzureWizardExecuteStep, type IAzureQuickPickItem, type ISubscriptionActionContext, type IWizardOptions } from "@microsoft/vscode-azext-utils";
import { localize } from "../../../localize";
import { type IBindingSetting } from "../../../templates/IBindingTemplate";
import { type IEventHubsConnectionWizardContext } from "../../appSettings/connectionSettings/eventHubs/IEventHubsConnectionWizardContext";
import { type IBindingWizardContext } from "../IBindingWizardContext";
import { StorageConnectionCreateStep } from "./StorageConnectionCreateStep";

export class StorageTypePromptStep extends AzureWizardPromptStep<IBindingWizardContext> {
private readonly _setting: IBindingSetting;

constructor(setting: IBindingSetting) {
super();
this._setting = setting;
}

public async prompt(context: IBindingWizardContext): Promise<void> {
const picks: IAzureQuickPickItem<boolean>[] = [
{ label: localize('useAzurite', 'Use Azurite emulator for local storage'), data: true },
{ label: localize('useAzureStorage', 'Use Azure Storage for remote storage'), data: false }
];

context.useStorageEmulator = (await context.ui.showQuickPick(picks, { placeHolder: localize('selectStorage', 'Select a storage account type for development') })).data;
return;
}

public shouldPrompt(context: IBindingWizardContext): boolean {
return context.useStorageEmulator === undefined;
}

public async getSubWizard(context: IBindingWizardContext): Promise<IWizardOptions<IBindingWizardContext> | undefined> {
const promptSteps: AzureWizardPromptStep<IBindingWizardContext & ISubscriptionActionContext & IEventHubsConnectionWizardContext>[] = [];
const executeSteps: AzureWizardExecuteStep<IBindingWizardContext & ISubscriptionActionContext & IEventHubsConnectionWizardContext>[] = [];

if (!context.useStorageEmulator) {
promptSteps.push(new StorageAccountListStep(
{ kind: StorageAccountKind.Storage, performance: StorageAccountPerformance.Standard, replication: StorageAccountReplication.LRS },
{ kind: [StorageAccountKind.BlobStorage], learnMoreLink: 'https://aka.ms/T5o0nf' }
));
}

executeSteps.push(new StorageConnectionCreateStep(this._setting));

return {
promptSteps,
executeSteps
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { KnownAccessRights, type AccessKeys, type AuthorizationRule, type EventH
import { type StorageAccount, type StorageAccountListKeysResult, type StorageManagementClient } from "@azure/arm-storage";
import { getResourceGroupFromId, uiUtils, type IStorageAccountWizardContext } from "@microsoft/vscode-azext-azureutils";
import { nonNullProp, nonNullValue, type ISubscriptionContext } from "@microsoft/vscode-azext-utils";
import { localSettingsFileName } from "../../../constants";
import { ConnectionKey, localSettingsFileName, localStorageEmulatorConnectionString } from "../../../constants";
import { defaultDescription } from "../../../constants-nls";
import { ext } from "../../../extensionVariables";
import { localize } from "../../../localize";
Expand All @@ -21,11 +21,17 @@ export interface IResourceResult {
connectionString: string;
}

export async function getStorageConnectionString(context: IStorageAccountWizardContext): Promise<IResourceResult> {
export async function getStorageConnectionString(context: IStorageAccountWizardContext & { useStorageEmulator?: boolean }): Promise<IResourceResult> {
if (context.useStorageEmulator) {
return {
name: ConnectionKey.Storage,
connectionString: localStorageEmulatorConnectionString
}
}

const client: StorageManagementClient = await createStorageClient(context);
const storageAccount: StorageAccount = nonNullProp(context, 'storageAccount');
const name: string = nonNullProp(storageAccount, 'name');

const resourceGroup: string = getResourceGroupFromId(nonNullProp(storageAccount, 'id'));
const result: StorageAccountListKeysResult = await client.storageAccounts.listKeys(resourceGroup, name);
const key: string = nonNullProp(nonNullValue(nonNullProp(result, 'keys')[0], 'keys[0]'), 'value');
Expand Down
2 changes: 0 additions & 2 deletions src/commands/registerCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import { deleteServiceConnector } from '../serviceConnector/deleteServiceConnect
import { validateServiceConnector } from '../serviceConnector/validateServiceConnector';
import { ResolvedFunctionAppResource } from '../tree/ResolvedFunctionAppResource';
import { addBinding } from './addBinding/addBinding';
import { setAzureWebJobsStorage } from './appSettings/connectionSettings/azureWebJobsStorage/setAzureWebJobsStorage';
import { downloadAppSettings } from './appSettings/downloadAppSettings';
import { decryptLocalSettings } from './appSettings/localSettings/decryptLocalSettings';
import { encryptLocalSettings } from './appSettings/localSettings/encryptLocalSettings';
Expand Down Expand Up @@ -93,7 +92,6 @@ export function registerCommands(): void {
registerCommandWithTreeNodeUnwrapping('azureFunctions.pickProcess', pickFuncProcess);
registerCommandWithTreeNodeUnwrapping('azureFunctions.redeploy', redeployDeployment);
registerCommandWithTreeNodeUnwrapping('azureFunctions.restartFunctionApp', restartFunctionApp);
registerCommandWithTreeNodeUnwrapping('azureFunctions.setAzureWebJobsStorage', setAzureWebJobsStorage);
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't we still need this for the command palette to work?

Copy link
Member Author

Choose a reason for hiding this comment

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

You are absolutely correct. Forgot to revert that one. 🤦

registerCommandWithTreeNodeUnwrapping('azureFunctions.startFunctionApp', startFunctionApp);
registerCommandWithTreeNodeUnwrapping('azureFunctions.startJavaRemoteDebug', remoteDebugJavaFunctionApp);
registerCommandWithTreeNodeUnwrapping('azureFunctions.startRemoteDebug', startRemoteDebug);
Expand Down
Loading