diff --git a/src/commands/createContainerApp/setQuickStartImage.ts b/src/commands/createContainerApp/setQuickStartImage.ts index eb91017d5..7d49c2633 100644 --- a/src/commands/createContainerApp/setQuickStartImage.ts +++ b/src/commands/createContainerApp/setQuickStartImage.ts @@ -3,10 +3,11 @@ * Licensed under the MIT License. See License.md in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { quickStartImageName } from "../../constants"; import { IContainerAppContext } from "./IContainerAppContext"; export function setQuickStartImage(context: Partial): void { - context.image = 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest'; + context.image = quickStartImageName; context.enableIngress = true; context.enableExternal = true; context.targetPort = 80; diff --git a/src/commands/imageSource/containerRegistry/acr/AcrListStep.ts b/src/commands/imageSource/containerRegistry/acr/AcrListStep.ts index d4dab6876..ed9f6e833 100644 --- a/src/commands/imageSource/containerRegistry/acr/AcrListStep.ts +++ b/src/commands/imageSource/containerRegistry/acr/AcrListStep.ts @@ -6,10 +6,14 @@ import type { ContainerRegistryManagementClient, Registry } from "@azure/arm-containerregistry"; import { uiUtils } from "@microsoft/vscode-azext-azureutils"; import { AzureWizardPromptStep, IAzureQuickPickItem, IWizardOptions } from "@microsoft/vscode-azext-utils"; +import { acrDomain, currentlyDeployed, quickStartImageName } from "../../../../constants"; +import type { ContainerAppModel } from "../../../../tree/ContainerAppItem"; import { createContainerRegistryManagementClient } from "../../../../utils/azureClients"; +import { parseImageName } from "../../../../utils/imageNameUtils"; import { localize } from "../../../../utils/localize"; import { nonNullProp } from "../../../../utils/nonNull"; import { IContainerRegistryImageContext } from "../IContainerRegistryImageContext"; +import { getLatestContainerAppImage } from "../getLatestContainerImage"; import { RegistryEnableAdminUserStep } from "./RegistryEnableAdminUserStep"; export class AcrListStep extends AzureWizardPromptStep { @@ -32,8 +36,31 @@ export class AcrListStep extends AzureWizardPromptStep[]> { const client: ContainerRegistryManagementClient = await createContainerRegistryManagementClient(context); - const registries = await uiUtils.listAllIterator(client.registries.list()); - return registries.map((r) => { return { label: nonNullProp(r, 'name'), data: r, description: r.loginServer } }); + const registries: Registry[] = await uiUtils.listAllIterator(client.registries.list()); + + const containerApp: ContainerAppModel = nonNullProp(context, 'targetContainer'); + const { registryDomain, registryName, imageNameReference } = parseImageName(getLatestContainerAppImage(containerApp)); + + // If the image is not the default quickstart image, then we can try to suggest a registry based on the latest Container App image + let suggestedRegistry: string | undefined; + if (registryDomain === acrDomain && imageNameReference !== quickStartImageName) { + suggestedRegistry = registryName; + } + + // Does the suggested registry exist in the list of pulled registries? If so, move it to the front of the list + const srIndex: number = registries.findIndex((r) => !!suggestedRegistry && r.loginServer === suggestedRegistry); + const srExists: boolean = srIndex !== -1; + if (srExists) { + const sr: Registry = registries.splice(srIndex, 1)[0]; + registries.unshift(sr); + } + + // Preferring 'suppressPersistence: true' over 'priority: highest' to avoid the possibility of a double parenthesis appearing in the description + return registries.map((r) => { + return !!suggestedRegistry && r.loginServer === suggestedRegistry ? + { label: nonNullProp(r, 'name'), data: r, description: `${r.loginServer} ${currentlyDeployed}`, suppressPersistence: true } : + { label: nonNullProp(r, 'name'), data: r, description: r.loginServer, suppressPersistence: srExists }; + }); } } diff --git a/src/commands/imageSource/containerRegistry/acr/AcrRepositoriesListStep.ts b/src/commands/imageSource/containerRegistry/acr/AcrRepositoriesListStep.ts index be729e515..2283e4d5a 100644 --- a/src/commands/imageSource/containerRegistry/acr/AcrRepositoriesListStep.ts +++ b/src/commands/imageSource/containerRegistry/acr/AcrRepositoriesListStep.ts @@ -4,16 +4,42 @@ *--------------------------------------------------------------------------------------------*/ import { uiUtils } from "@microsoft/vscode-azext-azureutils"; import { QuickPickItem } from "vscode"; +import { acrDomain, currentlyDeployed, quickStartImageName } from "../../../../constants"; +import type { ContainerAppModel } from "../../../../tree/ContainerAppItem"; import { createContainerRegistryClient } from "../../../../utils/azureClients"; -import { nonNullValue } from "../../../../utils/nonNull"; +import { parseImageName } from "../../../../utils/imageNameUtils"; +import { nonNullProp, nonNullValue } from "../../../../utils/nonNull"; import { IContainerRegistryImageContext } from "../IContainerRegistryImageContext"; import { RegistryRepositoriesListStepBase } from "../RegistryRepositoriesListBaseStep"; +import { getLatestContainerAppImage } from "../getLatestContainerImage"; export class AcrRepositoriesListStep extends RegistryRepositoriesListStepBase { public async getPicks(context: IContainerRegistryImageContext): Promise { const client = createContainerRegistryClient(context, nonNullValue(context.registry)); const repositoryNames: string[] = await uiUtils.listAllIterator(client.listRepositoryNames()); - return repositoryNames.map((rn) => { return { label: rn } }); + const containerApp: ContainerAppModel = nonNullProp(context, 'targetContainer'); + const { registryDomain, repositoryName, imageNameReference } = parseImageName(getLatestContainerAppImage(containerApp)); + + // If the image is not the default quickstart image, then we can try to suggest a repository based on the latest Container App image + let suggestedRepository: string | undefined; + if (registryDomain === acrDomain && imageNameReference !== quickStartImageName) { + suggestedRepository = repositoryName; + } + + // Does the suggested repositoryName exist in the list of pulled repositories? If so, move it to the front of the list + const srIndex: number = repositoryNames.findIndex((rn) => !!suggestedRepository && rn === suggestedRepository); + const srExists: boolean = srIndex !== -1; + if (srExists) { + const sr: string = repositoryNames.splice(srIndex, 1)[0]; + repositoryNames.unshift(sr); + } + + // Preferring 'suppressPersistence: true' over 'priority: highest' to avoid the possibility of a double parenthesis appearing in the description + return repositoryNames.map((rn) => { + return !!suggestedRepository && rn === suggestedRepository ? + { label: rn, description: currentlyDeployed, suppressPersistence: true } : + { label: rn, suppressPersistence: srExists }; + }); } } diff --git a/src/commands/imageSource/containerRegistry/getLatestContainerImage.ts b/src/commands/imageSource/containerRegistry/getLatestContainerImage.ts new file mode 100644 index 000000000..7bf7dd3f0 --- /dev/null +++ b/src/commands/imageSource/containerRegistry/getLatestContainerImage.ts @@ -0,0 +1,11 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type { ContainerApp } from "@azure/arm-appcontainers"; + +export function getLatestContainerAppImage(containerApp: ContainerApp): string | undefined { + // We are currently only supporting one active container image per app + return containerApp.template?.containers?.[0]?.image; +} diff --git a/src/constants.ts b/src/constants.ts index a61937bfe..35c04e394 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -31,6 +31,8 @@ export namespace RevisionConstants { export const single: IAzureQuickPickItem = { label: localize('single', 'Single'), description: localize('singleDesc', 'One active revision at a time'), data: 'single' }; } +export const currentlyDeployed: string = localize('currentlyDeployed', '(currently deployed)'); + export enum ScaleRuleTypes { HTTP = "HTTP scaling", Queue = "Azure queue" @@ -60,6 +62,7 @@ export type ImageSourceValues = typeof ImageSource[keyof typeof ImageSource]; export const acrDomain = 'azurecr.io'; export const dockerHubDomain = 'docker.io'; export const dockerHubRegistry = 'index.docker.io'; +export const quickStartImageName = 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest'; export type SupportedRegistries = 'azurecr.io' | 'docker.io'; diff --git a/src/utils/imageNameUtils.ts b/src/utils/imageNameUtils.ts index ae8d9f4a3..9edda21ca 100644 --- a/src/utils/imageNameUtils.ts +++ b/src/utils/imageNameUtils.ts @@ -6,9 +6,50 @@ import type { ContainerRegistryManagementClient, Registry } from "@azure/arm-containerregistry"; import { uiUtils } from "@microsoft/vscode-azext-azureutils"; import { ISubscriptionActionContext } from "@microsoft/vscode-azext-utils"; -import { acrDomain, dockerHubDomain, SupportedRegistries } from "../constants"; +import { SupportedRegistries, acrDomain, dockerHubDomain } from "../constants"; import { createContainerRegistryManagementClient } from "./azureClients"; +interface ParsedImageName { + imageNameReference?: string; + registryDomain?: SupportedRegistries; + registryName?: string; + namespace?: string; + repositoryName?: string; + tag?: string; +} + +/** + * @param imageName The full image name, including any registry, namespace, repository, and tag + * + * @example + * Format: '/<...namespaces>/:' + * ACR: 'acrRegistryName.azurecr.io/repositoryName:tag' + * DH: 'docker.io/namespace/repositoryName:tag' + * + * @returns A 'ParsedImageName' with the following properties: + * (1) 'imageNameReference': The original full image name; + * (2) 'registryDomain': The 'SupportedRegistries' domain, if it can be determined from the 'registryName'; + * (3) 'registryName': Everything before the first slash; + * (4) 'namespace': Everything between the 'registryName' and the 'repositoryName', including intermediate slashes; + * (5) 'repositoryName': Everything after the last slash (until the tag, if it is present); + * (6) 'tag': Everything after the ":", if it is present + */ +export function parseImageName(imageName?: string): ParsedImageName { + if (!imageName) { + return {}; + } + + const match: RegExpMatchArray | null = imageName.match(/^(?:(?[^/]+)\/)?(?:(?[^/]+(?:\/[^/]+)*)\/)?(?[^/:]+)(?::(?[^/]+))?$/); + return { + imageNameReference: imageName, + registryDomain: match?.groups?.registryName ? detectRegistryDomain(match.groups.registryName) : undefined, + registryName: match?.groups?.registryName, + namespace: match?.groups?.namespace, + repositoryName: match?.groups?.repositoryName, + tag: match?.groups?.tag + }; +} + /** * @param registryName When parsed from a full image name, everything before the first slash */