Skip to content

Add Local and Remote Settings conversion#4387

Merged
nturinski merged 36 commits intomainfrom
meganmott/convertSettings
Mar 28, 2025
Merged

Add Local and Remote Settings conversion#4387
nturinski merged 36 commits intomainfrom
meganmott/convertSettings

Conversation

@motm32
Copy link
Copy Markdown
Contributor

@motm32 motm32 commented Feb 28, 2025

Fixes #4376

For both local and remote there are multiple entry points:
Local
On the top level Local Project node in the workspace view and on each connection under the local settings node:
image
I have also added a warning node on the specific connections that need to be converted.

Remote
On the top level App Setting Item and on the specific connection:
image

After converting connections and assigning roles to the chosen managed identity the activity log will look like this:
image

Notes:

  • We are not deleting local or remote settings as this can break users apps as changes to the trigger code needs to be made in order for it to work. We currently are just adding the new settings and setting up the roles for the remote
  • We cannot add roles for Cosmos DB triggers as they have their own RBAC roles see here for more information
  • When locally debugging role assignments aren't necessary as your local machine cannot access the managed identity.

Todo:

  • Release appsettings and azure utils package and update the package json
  • Test each connection type
  • When right clicking on a remote app setting the command should only show if there are connections to be converted

@motm32 motm32 requested a review from a team as a code owner February 28, 2025 18:19
@microsoft microsoft deleted a comment Mar 3, 2025
context.activityChildren?.push(
new GenericTreeItem(undefined, {
contextValue: createUniversallyUniqueContextValue(['useExistingResourceGroupInfoItem', activitySuccessContext]),
label: localize('deletedSetting', `Deleted app setting "${connection.originalValue}"`),
Copy link
Copy Markdown
Contributor

@MicroFish91 MicroFish91 Mar 4, 2025

Choose a reason for hiding this comment

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

Should all the activity child labels be present tense instead of past tense?

Left a similar comment on this PR as well with more context:
#4382 (comment)

Copy link
Copy Markdown
Member

@nturinski nturinski left a comment

Choose a reason for hiding this comment

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

🥲

Comment thread package.nls.json Outdated
"azureFunctions.createNewProjectWithDockerfile": "Create New Containerized Project...",
"azureFunctions.createPythonVenv": "Create a virtual environment when creating a new Python project.",
"azureFunctions.createSlot": "Create Slot...",
"azureFunctions.convertLocalConnections": "Convert Local Project Connections to Identity-Based Connections...",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We may want to change the verbiage from "convert" to "add" considering we aren't deleting the old settings anymore.

import { type IConvertConnectionsContext } from "./IConvertConnectionsContext";
import { type Connection } from "./SelectConnectionsStep";

export class ConvertSettingsStep extends AzureWizardExecuteStep<IConvertConnectionsContext> {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: I prefer having the step names following a <noun><verb>Step convention. The reason being, if you have multiple steps that are for Settings, they would all be listed together in the folder, like SettingsConvertStep, SettingsListStep, etc.

public priority: number = 100;

public async execute(context: IConvertConnectionsContext): Promise<void> {
if (context.connections) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'd prefer to take this out of the if statement and just do nonNullProp(context, connections).

The reason being that this should only execute if !!context.connections evaluates to true, and if there is some reason that this is running when it is not, I'd rather the step throw an internal error than just silently skipping it, making it a harder to uncover error.

...getClientIdAndCredentialProperties(context, storageAccountName)
);
context.roles?.push({
scopeId: await getScopeHelper(context, storageAccountName, `resourceType eq 'Microsoft.Storage/storageAccounts'`),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is the filter supposed to be resourceType eq on all of these? Can you explain this a little bit?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Within the scope helper function I only want to look at resources that are the same resource type as the settings i am looking at. This is because resources can have the same names and that is the only information I have to find which resource the scope should be assigned to.

roleDefinitionId: createRoleId(context.subscriptionId, CommonRoleDefinitions.storageBlobDataOwner),
roleDefinitionName: CommonRoleDefinitions.storageBlobDataOwner.roleName
});
} else if (connection.name.includes('STORAGE')) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think it's safe to combine this and connection.name.includes('AzureWebJobsStorage') as one conditional?

if (connection.name.includes('AzureWebJobsStorage') || connection.name.includes('STORAGE') to save on a lot of space/copied logic.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah I thought about doing this. Originally I had AzureWebJobsStorage set to the __accountName property and only recently found out we shouldn't be using that. I'll combine the two.

return picks;
}

private async getRemoteQuickPics(context: IConvertConnectionsContext): Promise<IAzureQuickPickItem<Connection>[]> {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit:

Suggested change
private async getRemoteQuickPics(context: IConvertConnectionsContext): Promise<IAzureQuickPickItem<Connection>[]> {
private async getRemoteQuickPicks(context: IConvertConnectionsContext): Promise<IAzureQuickPickItem<Connection>[]> {

private async getRemoteQuickPics(context: IConvertConnectionsContext): Promise<IAzureQuickPickItem<Connection>[]> {
const picks: IAzureQuickPickItem<Connection>[] = [];

if (context.functionapp) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Similar thing to what I had said in another step, but I'd prefer you use nonNullValue here because otherwise, it'll just return an empty array and we won't know if that's because there is no function app or if it's because the function app isn't listing app settings correctly.

export async function convertLocalConnections(context: IActionContext, node?: AppSettingTreeItem): Promise<void> {
const connections: Connection[] = [];
if (node instanceof AppSettingTreeItem) {
connections.push({ name: node.id, value: node.value })
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
connections.push({ name: node.id, value: node.value })
connections.push({ name: node.id, value: node.value });


const continueOn: MessageItem = { title: localize('continueOn', 'Continue') };
const message: string = localize('setConnectionsProperty', 'Successfully converted local connections in order to use identity based connections you may need to set the connection property within your trigger.');
await context.ui.showWarningMessage(message, { learnMoreLink: "https://aka.ms/AAuroke", modal: true }, continueOn);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I believe that we should show this message before they start converting. We can show it again after in the activity log, but if they're worried about messing up their project, it's a little bit late by this point.

}
}

export async function convertRemoteConnectionsInternal(context: IActionContext, connections?: Connection[], node?: AppSettingTreeItem): Promise<void> {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Are these internal functions identical besides setting local to false?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

There are some slight differences beyond that but once I split up the ConvertSettingsStep into remote and local they will also use different execute steps.

context.localSettingsPath = await getLocalSettingsFile(context, message);
}

const localSettings = await AzExtFsExtra.readJSON<ILocalSettingsJson>(nonNullProp(context, 'localSettingsPath'));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I feel like we should just make getting the settings from the local.settings.json a helper function.

import { type SlotTreeItem } from "../../tree/SlotTreeItem";
import { type Connection } from "./ConnectionsListStep";

export interface IAddMIConnectionsContext extends ExecuteActivityContext, IResourceGroupWizardContext, ISubscriptionActionContext {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: No "👁️" needed here.................................

public priority: number = 110;

public async execute(context: IAddMIConnectionsContext): Promise<void> {
if (!context.functionapp) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: Why not just use nonNullProp?

Comment thread src/commands/addMIConnections/RemoteSettingsAddStep.ts
Comment thread src/commands/addMIConnections/RemoteSettingsAddStep.ts
Comment thread src/commands/addMIConnections/addLocalMIConnections.ts
const promptSteps: AzureWizardPromptStep<IAddMIConnectionsContext>[] = [];
const executeSteps: AzureWizardExecuteStep<IAddMIConnectionsContext>[] = [];

promptSteps.push(new ConnectionsListStep())
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: You're not pushing steps in dynamically anymore so might as well just instantiate promptSteps and executeSteps with the steps that they need instead of creating empty arrays first,

await node.runWithTemporaryDescription(context, localize('adding', 'Adding...'), async () => {
await addRemoteMIConnectionsInternal(context, undefined, node);
});
} else {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why do you need this else? What's the third case here?

await context.ui.showWarningMessage(localize('rolesWillBeAssignedMessage', 'This command will assign a managed identity and roles would you like to continue?'), { modal: true }, continueOn);
await wizard.execute();

const message: string = localize('setConnectionsProperty', 'Successfully added remote connections in order to use identity connections your application may require additional permisssions based on your code. You also may need to modify the connection property within your trigger.');
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit:

import { ext } from '../extensionVariables';
import { installOrUpdateFuncCoreTools } from '../funcCoreTools/installOrUpdateFuncCoreTools';
import { uninstallFuncCoreTools } from '../funcCoreTools/uninstallFuncCoreTools';
import { type DurableTaskSchedulerClient } from '../tree/durableTaskScheduler/DurableTaskSchedulerClient';
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You really just went and reorganized this whole file huh?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

💩

}

private async getPicks(context: AddMIConnectionsContext): Promise<IAzureQuickPickItem<Connection>[]> {
if (context.functionapp) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: return context.functionapp ? this.getRemoteQuickPicks(context) : this.getLocalQuickPicks(context)

Comment thread src/commands/addMIConnections/ConnectionsListStep.ts
const localSettingsPath: string = await getLocalSettingsFile(context, message, workspaceFolder);
context.localSettingsPath = localSettingsPath;

if (await AzExtFsExtra.pathExists(localSettingsPath)) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Now that you're using this function, I think you can get rid of the if statement above.

await wizard.execute();

const continueOn: MessageItem = { title: localize('continueOn', 'Continue') };
const message: string = localize('setConnectionsProperty', 'Successfully added local connections. In order to use identity based connections, you may need to set the connection properties within your trigger.');
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Successfully added local connections. To use identity-based connections, you may need to configure the connection properties within your trigger.

if (localSettingsPath) {
if (await AzExtFsExtra.pathExists(localSettingsPath)) {
const localSettingsUri: vscode.Uri = vscode.Uri.file(localSettingsPath);
let localSettings: ILocalSettingsJson = <ILocalSettingsJson>await AzExtFsExtra.readJSON(localSettingsPath);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Use your helper function here.


const localSettings = await getLocalSettingsJson(context, localSettingsPath);
if (localSettings.Values) {
for (const [key, value] of Object.entries(localSettings.Values)) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: If you want to be even cleaner, this can be a helper function since it's used by getRemoteQuickPicks too (don't hate me)

const localSettingsPath: string | undefined = await getLocalSettingsFileNoPrompt(context, this._workspaceFolder);
if (localSettingsPath === undefined) {
return { properties: {} };
} else {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You can use your helper function again in here (a couple lines below this).

It also may make more sense to export getLocalSettingsJson from here since it's more related to the LocalSettingsClient than anything else. You could consider adding it to a util file too.

import { SettingsAddBaseStep } from "./SettingsAddBaseStep";

export async function addLocalMIConnections(context: IActionContext, node?: AppSettingTreeItem): Promise<void> {
const connections: Connection[] = [];
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think it's possible to have multiple local project nodes, so if you enter through the command palette, you'll need something like:

if (!node) {
  node = await pickLocalProject(); // not real code
}

}
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: Extra line breaks.

@nturinski nturinski enabled auto-merge (squash) March 28, 2025 22:18
@nturinski nturinski merged commit d62c7bb into main Mar 28, 2025
2 checks passed
@nturinski nturinski deleted the meganmott/convertSettings branch March 28, 2025 22:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

There are no visibility icons for local settings

3 participants