diff --git a/src/commands/imageSource/containerRegistry/ContainerRegistryListStep.ts b/src/commands/imageSource/containerRegistry/ContainerRegistryListStep.ts index e8b290b15..4b31cb9e2 100644 --- a/src/commands/imageSource/containerRegistry/ContainerRegistryListStep.ts +++ b/src/commands/imageSource/containerRegistry/ContainerRegistryListStep.ts @@ -23,7 +23,7 @@ export class ContainerRegistryListStep extends AzureWizardPromptStep[] = []; - picks.push({ label: 'Azure Container Registries', data: acrDomain }); + picks.push({ label: 'Azure Container Registry', data: acrDomain }); if (env.uiKind === UIKind.Desktop) { // this will fails in vscode.dev due to browser CORS access policies picks.push({ label: 'Docker Hub Registry', data: dockerHubDomain }); @@ -35,7 +35,7 @@ export class ContainerRegistryListStep extends AzureWizardPromptStep | undefined> { diff --git a/src/commands/imageSource/containerRegistry/RegistryImageInputStep.ts b/src/commands/imageSource/containerRegistry/RegistryImageInputStep.ts index 7778464b3..4db00c7d1 100644 --- a/src/commands/imageSource/containerRegistry/RegistryImageInputStep.ts +++ b/src/commands/imageSource/containerRegistry/RegistryImageInputStep.ts @@ -4,16 +4,32 @@ *--------------------------------------------------------------------------------------------*/ import { AzureWizardPromptStep } from "@microsoft/vscode-azext-utils"; +import { acrDomain, quickStartImageName } from "../../../constants"; +import { parseImageName } from "../../../utils/imageNameUtils"; import { localize } from "../../../utils/localize"; import { IContainerRegistryImageContext } from "./IContainerRegistryImageContext"; +import { getLatestContainerAppImage } from "./getLatestContainerImage"; export class RegistryImageInputStep extends AzureWizardPromptStep { public async prompt(context: IContainerRegistryImageContext): Promise { const prompt: string = localize('registryImagePrompt', 'Enter the container image with tag'); - const placeHolder: string = localize('registryImagePlaceHolder', 'For example: `mcr.microsoft.com/azuredocs/containerapps-helloworld:latest`') + const placeHolder: string = localize('registryImagePlaceHolder', 'For example: `mcr.microsoft.com/azuredocs/containerapps-helloworld:latest`'); + + // Try to suggest an image name only when the user is deploying to a Container App + let value: string | undefined; + if (context.targetContainer) { + const { registryDomain, imageNameReference } = parseImageName(getLatestContainerAppImage(context.targetContainer)); + + // Only bother carrying over the suggestion if the old image was from a third party registry + if (registryDomain !== acrDomain && imageNameReference !== quickStartImageName) { + value = imageNameReference; + } + } + context.image = (await context.ui.showInputBox({ prompt, placeHolder, + value })).trim(); context.valuesToMask.push(context.image); diff --git a/src/commands/imageSource/containerRegistry/dockerHub/DockerHubContainerRepositoryListStep.ts b/src/commands/imageSource/containerRegistry/dockerHub/DockerHubContainerRepositoryListStep.ts index 1142c0222..5a01f794e 100644 --- a/src/commands/imageSource/containerRegistry/dockerHub/DockerHubContainerRepositoryListStep.ts +++ b/src/commands/imageSource/containerRegistry/dockerHub/DockerHubContainerRepositoryListStep.ts @@ -3,13 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { QuickPickItem } from "vscode"; -import { loadMoreQp, QuickPicksCache } from "../../../../constants"; +import type { QuickPickItem } from "vscode"; +import { currentlyDeployed, dockerHubDomain, loadMoreQp, QuickPicksCache, quickStartImageName } from "../../../../constants"; +import { parseImageName } from "../../../../utils/imageNameUtils"; import { localize } from "../../../../utils/localize"; import { nonNullProp } from "../../../../utils/nonNull"; +import { getLatestContainerAppImage } from "../getLatestContainerImage"; import { IContainerRegistryImageContext } from "../IContainerRegistryImageContext"; import { RegistryRepositoriesListStepBase } from "../RegistryRepositoriesListBaseStep"; import { getReposForNamespace } from "./DockerHubV2ApiCalls"; +import type { DockerHubV2Repository } from "./DockerHubV2Types"; export class DockerHubContainerRepositoryListStep extends RegistryRepositoriesListStepBase { public async getPicks(context: IContainerRegistryImageContext, cachedPicks: QuickPicksCache): Promise { @@ -19,7 +22,33 @@ export class DockerHubContainerRepositoryListStep extends RegistryRepositoriesLi await context.ui.showWarningMessage(localize('noRepos', 'Unable to find any repositories associated to namespace "{0}"', context.dockerHubNamespace), { modal: true }); } - cachedPicks.cache.push(...response.results.map((r) => { return { label: r.name, description: r.description } })); + // Try to suggest a repository only when the user is deploying to a Container App + let suggestedRepository: string | undefined; + let srExists: boolean = false; + if (context.targetContainer) { + const { registryDomain, repositoryName, imageNameReference } = parseImageName(getLatestContainerAppImage(context.targetContainer)); + + // If the image is not the default quickstart image, then we can try to suggest a repository based on the latest Container App image + if (registryDomain === dockerHubDomain && 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 = response.results.findIndex((r) => !!suggestedRepository && r.name === suggestedRepository); + srExists = srIndex !== -1; + if (srExists) { + const sr: DockerHubV2Repository = response.results.splice(srIndex, 1)[0]; + response.results.unshift(sr); + } + } + + // Preferring 'suppressPersistence: true' over 'priority: highest' to avoid the possibility of a double parenthesis appearing in the description + const quickPicks: QuickPickItem[] = response.results.map((r) => { + return !!suggestedRepository && r.name === suggestedRepository ? + { label: r.name, description: r.description ? `${r.description} ${currentlyDeployed}` : currentlyDeployed, suppressPersistence: true } : + { label: r.name, description: r.description, suppressPersistence: srExists } + }); + cachedPicks.cache.push(...quickPicks); if (response.next) { cachedPicks.next = response.next; diff --git a/src/commands/imageSource/containerRegistry/dockerHub/DockerHubNamespaceInputStep.ts b/src/commands/imageSource/containerRegistry/dockerHub/DockerHubNamespaceInputStep.ts index 4107d22a3..d6347e1b6 100644 --- a/src/commands/imageSource/containerRegistry/dockerHub/DockerHubNamespaceInputStep.ts +++ b/src/commands/imageSource/containerRegistry/dockerHub/DockerHubNamespaceInputStep.ts @@ -4,16 +4,18 @@ *--------------------------------------------------------------------------------------------*/ import { AzureWizardPromptStep } from "@microsoft/vscode-azext-utils"; +import { dockerHubDomain, quickStartImageName } from "../../../../constants"; +import { parseImageName } from "../../../../utils/imageNameUtils"; import { localize } from "../../../../utils/localize"; import { IContainerRegistryImageContext } from "../IContainerRegistryImageContext"; +import { getLatestContainerAppImage } from "../getLatestContainerImage"; -let checkNameLength: boolean = false; export class DockerHubNamespaceInputStep extends AzureWizardPromptStep { public async prompt(context: IContainerRegistryImageContext): Promise { const prompt: string = localize('dockerHubNamespacePrompt', 'Enter a Docker Hub namespace'); context.dockerHubNamespace = (await context.ui.showInputBox({ prompt, - value: 'library', + value: this.getSuggestedNamespace(context), validateInput: async (value: string | undefined): Promise => await this.validateInput(value) })).toLowerCase(); @@ -26,16 +28,29 @@ export class DockerHubNamespaceInputStep extends AzureWizardPromptStep { name = name ? name.trim() : ''; - // to prevent showing an error when the character types the first letter - checkNameLength = checkNameLength || name.length > 1; const { minLength, maxLength } = { minLength: 4, maxLength: 30 }; if (/\W/.test(name)) { return localize('invalidNamespace', `A namespace name should only contain letters and/or numbers.`); - } else if ((checkNameLength && name.length < minLength) || name.length > maxLength) { + } else if (name.length < minLength || name.length > maxLength) { return localize('invalidLength', 'The name must be between {0} and {1} characters.', minLength, maxLength); } return undefined; } + + private getSuggestedNamespace(context: IContainerRegistryImageContext): string { + // Try to suggest a namespace only when the user is deploying to a Container App + let suggestedNamespace: string | undefined; + if (context.targetContainer) { + const { registryDomain, namespace, imageNameReference } = parseImageName(getLatestContainerAppImage(context.targetContainer)); + + // If the image is not the default quickstart image, then we can try to suggest a namespace based on the latest Container App image + if (registryDomain === dockerHubDomain && imageNameReference !== quickStartImageName) { + suggestedNamespace = namespace; + } + } + + return suggestedNamespace || 'library'; + } }