Skip to content
15 changes: 15 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,21 @@
{
"title": "Azure Container Apps",
"properties": {
"containerApps.deployWorkspaceProject.containerRegistryName": {
"scope": "machine-overridable",
"type": "string",
"description": "%containerApps.deployWorkspaceProject.containerRegistryName%"
},
"containerApps.deployWorkspaceProject.containerAppName": {
"scope": "machine-overridable",
"type": "string",
"description": "%containerApps.deployWorkspaceProject.containerAppName%"
},
"containerApps.deployWorkspaceProject.containerAppResourceGroupName": {
"scope": "machine-overridable",
"type": "string",
"description": "%containerApps.deployWorkspaceProject.containerAppResourceGroupName%"
},
"containerApps.enableOutputTimestamps": {
"type": "boolean",
"description": "%containerApps.enableOutputTimestamps%",
Expand Down
5 changes: 4 additions & 1 deletion package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,8 @@
"containerApps.openGitHubRepo": "Open Repo in GitHub",
"containerApps.startStreamingLogs": "Start Streaming Logs...",
"containerApps.stopStreamingLogs": "Stop Streaming Logs...",
"containerApps.createAcr": "Create Azure Container Registry..."
"containerApps.createAcr": "Create Azure Container Registry...",
"containerApps.deployWorkspaceProject.containerAppName": "When deploying from a local workspace project, the name of the target container app to deploy to.",
"containerApps.deployWorkspaceProject.containerAppResourceGroupName": "When deploying from a local workspace project, the name of the target container app's resource group.",
"containerApps.deployWorkspaceProject.containerRegistryName": "When deploying from a local workspace project, the name of the Azure Container Registry to use for storing and building images."
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import type { ContainerRegistryManagementClient, Registry } from "@azure/arm-containerregistry";
import { uiUtils } from "@microsoft/vscode-azext-azureutils";
import { AzureWizardPromptStep, IAzureQuickPickItem, IWizardOptions, nonNullProp } from "@microsoft/vscode-azext-utils";
import { AzureWizardPromptStep, IAzureQuickPickItem, ISubscriptionActionContext, IWizardOptions, nonNullProp } from "@microsoft/vscode-azext-utils";
import { acrDomain, currentlyDeployed, quickStartImageName } from "../../../../../constants";
import { createContainerRegistryManagementClient } from "../../../../../utils/azureClients";
import { parseImageName } from "../../../../../utils/imageNameUtils";
Expand Down Expand Up @@ -33,8 +33,7 @@ export class AcrListStep extends AzureWizardPromptStep<IContainerRegistryImageCo
}

public async getPicks(context: IContainerRegistryImageContext): Promise<IAzureQuickPickItem<Registry>[]> {
const client: ContainerRegistryManagementClient = await createContainerRegistryManagementClient(context);
const registries: Registry[] = await uiUtils.listAllIterator(client.registries.list());
const registries: Registry[] = await AcrListStep.getRegistries(context);

// Try to suggest a registry only when the user is deploying to a Container App
let suggestedRegistry: string | undefined;
Expand Down Expand Up @@ -63,5 +62,10 @@ export class AcrListStep extends AzureWizardPromptStep<IContainerRegistryImageCo
{ label: nonNullProp(r, 'name'), data: r, description: r.loginServer, suppressPersistence: srExists };
});
}

public static async getRegistries(context: ISubscriptionActionContext): Promise<Registry[]> {
const client: ContainerRegistryManagementClient = await createContainerRegistryManagementClient(context);
return await uiUtils.listAllIterator(client.registries.list());
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import type { WorkspaceFolder } from "vscode";
import { settingUtils } from "../../utils/settingUtils";

export interface DeployWorkspaceProjectSettings {
// Container app names are unique to a resource group
containerAppResourceGroupName?: string;
containerAppName?: string;

containerRegistryName?: string;
}

const deployWorkspaceProjectPrefix: string = 'deployWorkspaceProject';

export async function getDeployWorkspaceProjectSettings(rootFolder: WorkspaceFolder): Promise<DeployWorkspaceProjectSettings | undefined> {
const settingsPath: string = settingUtils.getDefaultRootWorkspaceSettingsPath(rootFolder);

try {
const containerAppName: string | undefined = settingUtils.getWorkspaceSetting(`${deployWorkspaceProjectPrefix}.containerAppName`, settingsPath);
const containerAppResourceGroupName: string | undefined = settingUtils.getWorkspaceSetting(`${deployWorkspaceProjectPrefix}.containerAppResourceGroupName`, settingsPath);
const containerRegistryName: string | undefined = settingUtils.getWorkspaceSetting(`${deployWorkspaceProjectPrefix}.containerRegistryName`, settingsPath);

if (containerAppName || containerAppResourceGroupName || containerRegistryName) {
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.

This isn't a blocker, but I think it's awkward to only return an object if one of the properties is defined. And otherwise we return undefined.

In the future, if you add more settings, then you need to add them to this if expression.

I think I'd prefer if you just always return an object, even if the object just contains undefined values.

return {
containerAppName,
containerAppResourceGroupName,
containerRegistryName
};
}
} catch { /** Do nothing */ }
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.

Might be useful to add a comment here that explains what errors are being ignored here and why

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.

Ok now that I see this again, you should probably log something inside that catch block too.

Copy link
Copy Markdown
Contributor Author

@MicroFish91 MicroFish91 Sep 21, 2023

Choose a reason for hiding this comment

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

This was a good suggestion, especially because it actually pushed me to test the get setting method a little bit more 😆

I realized that it doesn't actually throw an error even with bad settings path and property... so I removed the catch entirely. I also made a small change to the setting utils so that it reads a missing value as undefined rather than as an empty string '' (definitely not a behavior I expected it to have by default).

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.

Ok that's great


return undefined;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.md in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import type { Registry } from "@azure/arm-containerregistry";
import type { ISubscriptionActionContext } from "@microsoft/vscode-azext-utils";
import { ext } from "../../../extensionVariables";
import { localize } from "../../../utils/localize";
import { AcrListStep } from "../../deployImage/imageSource/containerRegistry/acr/AcrListStep";
import { DeployWorkspaceProjectSettings } from "../DeployWorkspaceProjectSettings";

interface DefaultAcrResources {
registry?: Registry;
imageName?: string;
}

export async function getDefaultAcrResources(context: ISubscriptionActionContext, settings: DeployWorkspaceProjectSettings | undefined): Promise<DefaultAcrResources> {
const noMatchingResource = { registry: undefined };

if (!settings || !settings.containerRegistryName) {
return noMatchingResource;
}

const registries: Registry[] = await AcrListStep.getRegistries(context);
const savedRegistry: Registry | undefined = registries.find(r => r.name === settings.containerRegistryName);

if (savedRegistry) {
ext.outputChannel.appendLog(localize('foundResourceMatch', 'Used saved workspace settings and found an existing container registry.'));
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.

Big fan of the logs in this function. You could include the found resource names in the log if you wanted to be even more verbose. But just an idea.

Copy link
Copy Markdown
Contributor Author

@MicroFish91 MicroFish91 Sep 20, 2023

Choose a reason for hiding this comment

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

This suggestion will be included in the following PR!

return {
registry: savedRegistry,
imageName: `${settings.containerAppName || savedRegistry.name}:latest`
};
} else {
ext.outputChannel.appendLog(localize('noResourceMatch', 'Used saved workspace settings to search for Azure Container Registry "{0}" but found no match.', settings.containerRegistryName));
return noMatchingResource;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.md in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { ContainerApp, ContainerAppsAPIClient, ManagedEnvironment } from "@azure/arm-appcontainers";
import type { ResourceGroup } from "@azure/arm-resources";
import { ResourceGroupListStep, uiUtils } from "@microsoft/vscode-azext-azureutils";
import type { ISubscriptionActionContext } from "@microsoft/vscode-azext-utils";
import { ext } from "../../../extensionVariables";
import { ContainerAppItem, ContainerAppModel } from "../../../tree/ContainerAppItem";
import { createContainerAppsAPIClient } from "../../../utils/azureClients";
import { localize } from "../../../utils/localize";
import { DeployWorkspaceProjectSettings } from "../DeployWorkspaceProjectSettings";

interface DefaultContainerAppsResources {
resourceGroup?: ResourceGroup;
managedEnvironment?: ManagedEnvironment;
containerApp?: ContainerAppModel;
}

export async function getDefaultContainerAppsResources(context: ISubscriptionActionContext, settings: DeployWorkspaceProjectSettings | undefined): Promise<DefaultContainerAppsResources> {
const noMatchingResources = {
resourceGroup: undefined,
managedEnvironment: undefined,
containerApp: undefined
};

if (!settings || !settings.containerAppResourceGroupName || !settings.containerAppName) {
return noMatchingResources;
}

const resourceGroupName: string = settings.containerAppResourceGroupName;
const containerAppName: string = settings.containerAppName;

try {
const client: ContainerAppsAPIClient = await createContainerAppsAPIClient(context)
const containerApp: ContainerApp = await client.containerApps.get(resourceGroupName, containerAppName);
const containerAppModel: ContainerAppModel = ContainerAppItem.CreateContainerAppModel(containerApp);

const managedEnvironments: ManagedEnvironment[] = await uiUtils.listAllIterator(client.managedEnvironments.listBySubscription());
const managedEnvironment = managedEnvironments.find(env => env.id === containerAppModel.managedEnvironmentId);

const resourceGroups: ResourceGroup[] = await ResourceGroupListStep.getResourceGroups(context);
const resourceGroup = resourceGroups.find(rg => rg.name === containerAppModel.resourceGroup);

ext.outputChannel.appendLog(localize('foundResourceMatch', 'Used saved workspace settings and found existing container app resources.'));

return {
resourceGroup,
managedEnvironment,
containerApp: containerAppModel
};
} catch {
ext.outputChannel.appendLog(localize('noResourceMatch', 'Used saved workspace settings to search for container app "{0}" in resource group "{1}" but found no match.', settings.containerAppName, settings.containerAppResourceGroupName));
return noMatchingResources;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,28 @@
*--------------------------------------------------------------------------------------------*/

import type { ISubscriptionActionContext } from "@microsoft/vscode-azext-utils";
import { relativeSettingsFilePath } from "../../../constants";
import { ext } from "../../../extensionVariables";
import { localize } from "../../../utils/localize";
import type { DeployWorkspaceProjectContext } from "../DeployWorkspaceProjectContext";
import { DeployWorkspaceProjectSettings, getDeployWorkspaceProjectSettings } from "../DeployWorkspaceProjectSettings";
import { getDefaultAcrResources } from "./getDefaultAcrResources";
import { getDefaultContainerAppsResources } from "./getDefaultContainerAppsResources";
import { getWorkspaceProjectPaths } from "./getWorkspaceProjectPaths";

export async function getDefaultContextValues(context: ISubscriptionActionContext): Promise<Partial<DeployWorkspaceProjectContext>> {
const { rootFolder, dockerfilePath } = await getWorkspaceProjectPaths(context);

// const settings: IDeployWorkspaceProjectSettings | undefined = await getDeployWorkspaceProjectSettings(rootFolder);
// if (!settings) {
// ext.outputChannel.appendLog(localize('noWorkspaceSettings', 'Scanned and found no matching resource settings at "{0}".', relativeSettingsFilePath));
// } else if (!settings.containerAppResourceGroupName || !settings.containerAppName || !settings.containerRegistryName) {
// ext.outputChannel.appendLog(localize('resourceSettingsIncomplete', 'Scanned and found incomplete container app resource settings at "{0}".', relativeSettingsFilePath));
// }
const settings: DeployWorkspaceProjectSettings | undefined = await getDeployWorkspaceProjectSettings(rootFolder);
if (!settings) {
ext.outputChannel.appendLog(localize('noWorkspaceSettings', 'Scanned and found no matching resource settings at "{0}".', relativeSettingsFilePath));
} else if (!settings.containerAppResourceGroupName || !settings.containerAppName || !settings.containerRegistryName) {
ext.outputChannel.appendLog(localize('resourceSettingsIncomplete', 'Scanned and found incomplete container app resource settings at "{0}".', relativeSettingsFilePath));
}

return {
// ...await getDefaultContainerAppsResources(context, settings),
// ...await getDefaultAzureContainerRegistry(context, settings),
...await getDefaultContainerAppsResources(context, settings),
...await getDefaultAcrResources(context, settings),
// newRegistrySku: KnownSkuName.Basic,
dockerfilePath,
// environmentVariables: await EnvironmentVariablesListStep.workspaceHasEnvFile() ? undefined : [],
Expand Down