Skip to content

Commit 1887a0c

Browse files
authored
Suggest values during steps when deploying image from ACR (#305)
1 parent e266ef4 commit 1887a0c

File tree

6 files changed

+115
-6
lines changed

6 files changed

+115
-6
lines changed

src/commands/createContainerApp/setQuickStartImage.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
* Licensed under the MIT License. See License.md in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6+
import { quickStartImageName } from "../../constants";
67
import { IContainerAppContext } from "./IContainerAppContext";
78

89
export function setQuickStartImage(context: Partial<IContainerAppContext>): void {
9-
context.image = 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest';
10+
context.image = quickStartImageName;
1011
context.enableIngress = true;
1112
context.enableExternal = true;
1213
context.targetPort = 80;

src/commands/imageSource/containerRegistry/acr/AcrListStep.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,14 @@
66
import type { ContainerRegistryManagementClient, Registry } from "@azure/arm-containerregistry";
77
import { uiUtils } from "@microsoft/vscode-azext-azureutils";
88
import { AzureWizardPromptStep, IAzureQuickPickItem, IWizardOptions } from "@microsoft/vscode-azext-utils";
9+
import { acrDomain, currentlyDeployed, quickStartImageName } from "../../../../constants";
10+
import type { ContainerAppModel } from "../../../../tree/ContainerAppItem";
911
import { createContainerRegistryManagementClient } from "../../../../utils/azureClients";
12+
import { parseImageName } from "../../../../utils/imageNameUtils";
1013
import { localize } from "../../../../utils/localize";
1114
import { nonNullProp } from "../../../../utils/nonNull";
1215
import { IContainerRegistryImageContext } from "../IContainerRegistryImageContext";
16+
import { getLatestContainerAppImage } from "../getLatestContainerImage";
1317
import { RegistryEnableAdminUserStep } from "./RegistryEnableAdminUserStep";
1418

1519
export class AcrListStep extends AzureWizardPromptStep<IContainerRegistryImageContext> {
@@ -32,8 +36,31 @@ export class AcrListStep extends AzureWizardPromptStep<IContainerRegistryImageCo
3236

3337
public async getPicks(context: IContainerRegistryImageContext): Promise<IAzureQuickPickItem<Registry>[]> {
3438
const client: ContainerRegistryManagementClient = await createContainerRegistryManagementClient(context);
35-
const registries = await uiUtils.listAllIterator(client.registries.list());
36-
return registries.map((r) => { return { label: nonNullProp(r, 'name'), data: r, description: r.loginServer } });
39+
const registries: Registry[] = await uiUtils.listAllIterator(client.registries.list());
40+
41+
const containerApp: ContainerAppModel = nonNullProp(context, 'targetContainer');
42+
const { registryDomain, registryName, imageNameReference } = parseImageName(getLatestContainerAppImage(containerApp));
43+
44+
// If the image is not the default quickstart image, then we can try to suggest a registry based on the latest Container App image
45+
let suggestedRegistry: string | undefined;
46+
if (registryDomain === acrDomain && imageNameReference !== quickStartImageName) {
47+
suggestedRegistry = registryName;
48+
}
49+
50+
// Does the suggested registry exist in the list of pulled registries? If so, move it to the front of the list
51+
const srIndex: number = registries.findIndex((r) => !!suggestedRegistry && r.loginServer === suggestedRegistry);
52+
const srExists: boolean = srIndex !== -1;
53+
if (srExists) {
54+
const sr: Registry = registries.splice(srIndex, 1)[0];
55+
registries.unshift(sr);
56+
}
57+
58+
// Preferring 'suppressPersistence: true' over 'priority: highest' to avoid the possibility of a double parenthesis appearing in the description
59+
return registries.map((r) => {
60+
return !!suggestedRegistry && r.loginServer === suggestedRegistry ?
61+
{ label: nonNullProp(r, 'name'), data: r, description: `${r.loginServer} ${currentlyDeployed}`, suppressPersistence: true } :
62+
{ label: nonNullProp(r, 'name'), data: r, description: r.loginServer, suppressPersistence: srExists };
63+
});
3764
}
3865
}
3966

src/commands/imageSource/containerRegistry/acr/AcrRepositoriesListStep.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,42 @@
44
*--------------------------------------------------------------------------------------------*/
55
import { uiUtils } from "@microsoft/vscode-azext-azureutils";
66
import { QuickPickItem } from "vscode";
7+
import { acrDomain, currentlyDeployed, quickStartImageName } from "../../../../constants";
8+
import type { ContainerAppModel } from "../../../../tree/ContainerAppItem";
79
import { createContainerRegistryClient } from "../../../../utils/azureClients";
8-
import { nonNullValue } from "../../../../utils/nonNull";
10+
import { parseImageName } from "../../../../utils/imageNameUtils";
11+
import { nonNullProp, nonNullValue } from "../../../../utils/nonNull";
912
import { IContainerRegistryImageContext } from "../IContainerRegistryImageContext";
1013
import { RegistryRepositoriesListStepBase } from "../RegistryRepositoriesListBaseStep";
14+
import { getLatestContainerAppImage } from "../getLatestContainerImage";
1115

1216
export class AcrRepositoriesListStep extends RegistryRepositoriesListStepBase {
1317
public async getPicks(context: IContainerRegistryImageContext): Promise<QuickPickItem[]> {
1418
const client = createContainerRegistryClient(context, nonNullValue(context.registry));
1519
const repositoryNames: string[] = await uiUtils.listAllIterator(client.listRepositoryNames());
1620

17-
return repositoryNames.map((rn) => { return { label: rn } });
21+
const containerApp: ContainerAppModel = nonNullProp(context, 'targetContainer');
22+
const { registryDomain, repositoryName, imageNameReference } = parseImageName(getLatestContainerAppImage(containerApp));
23+
24+
// If the image is not the default quickstart image, then we can try to suggest a repository based on the latest Container App image
25+
let suggestedRepository: string | undefined;
26+
if (registryDomain === acrDomain && imageNameReference !== quickStartImageName) {
27+
suggestedRepository = repositoryName;
28+
}
29+
30+
// Does the suggested repositoryName exist in the list of pulled repositories? If so, move it to the front of the list
31+
const srIndex: number = repositoryNames.findIndex((rn) => !!suggestedRepository && rn === suggestedRepository);
32+
const srExists: boolean = srIndex !== -1;
33+
if (srExists) {
34+
const sr: string = repositoryNames.splice(srIndex, 1)[0];
35+
repositoryNames.unshift(sr);
36+
}
37+
38+
// Preferring 'suppressPersistence: true' over 'priority: highest' to avoid the possibility of a double parenthesis appearing in the description
39+
return repositoryNames.map((rn) => {
40+
return !!suggestedRepository && rn === suggestedRepository ?
41+
{ label: rn, description: currentlyDeployed, suppressPersistence: true } :
42+
{ label: rn, suppressPersistence: srExists };
43+
});
1844
}
1945
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import type { ContainerApp } from "@azure/arm-appcontainers";
7+
8+
export function getLatestContainerAppImage(containerApp: ContainerApp): string | undefined {
9+
// We are currently only supporting one active container image per app
10+
return containerApp.template?.containers?.[0]?.image;
11+
}

src/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ export namespace RevisionConstants {
3131
export const single: IAzureQuickPickItem<string> = { label: localize('single', 'Single'), description: localize('singleDesc', 'One active revision at a time'), data: 'single' };
3232
}
3333

34+
export const currentlyDeployed: string = localize('currentlyDeployed', '(currently deployed)');
35+
3436
export enum ScaleRuleTypes {
3537
HTTP = "HTTP scaling",
3638
Queue = "Azure queue"
@@ -60,6 +62,7 @@ export type ImageSourceValues = typeof ImageSource[keyof typeof ImageSource];
6062
export const acrDomain = 'azurecr.io';
6163
export const dockerHubDomain = 'docker.io';
6264
export const dockerHubRegistry = 'index.docker.io';
65+
export const quickStartImageName = 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest';
6366

6467
export type SupportedRegistries = 'azurecr.io' | 'docker.io';
6568

src/utils/imageNameUtils.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,50 @@
66
import type { ContainerRegistryManagementClient, Registry } from "@azure/arm-containerregistry";
77
import { uiUtils } from "@microsoft/vscode-azext-azureutils";
88
import { ISubscriptionActionContext } from "@microsoft/vscode-azext-utils";
9-
import { acrDomain, dockerHubDomain, SupportedRegistries } from "../constants";
9+
import { SupportedRegistries, acrDomain, dockerHubDomain } from "../constants";
1010
import { createContainerRegistryManagementClient } from "./azureClients";
1111

12+
interface ParsedImageName {
13+
imageNameReference?: string;
14+
registryDomain?: SupportedRegistries;
15+
registryName?: string;
16+
namespace?: string;
17+
repositoryName?: string;
18+
tag?: string;
19+
}
20+
21+
/**
22+
* @param imageName The full image name, including any registry, namespace, repository, and tag
23+
*
24+
* @example
25+
* Format: '<registryName>/<...namespaces>/<repositoryName>:<tag>'
26+
* ACR: 'acrRegistryName.azurecr.io/repositoryName:tag'
27+
* DH: 'docker.io/namespace/repositoryName:tag'
28+
*
29+
* @returns A 'ParsedImageName' with the following properties:
30+
* (1) 'imageNameReference': The original full image name;
31+
* (2) 'registryDomain': The 'SupportedRegistries' domain, if it can be determined from the 'registryName';
32+
* (3) 'registryName': Everything before the first slash;
33+
* (4) 'namespace': Everything between the 'registryName' and the 'repositoryName', including intermediate slashes;
34+
* (5) 'repositoryName': Everything after the last slash (until the tag, if it is present);
35+
* (6) 'tag': Everything after the ":", if it is present
36+
*/
37+
export function parseImageName(imageName?: string): ParsedImageName {
38+
if (!imageName) {
39+
return {};
40+
}
41+
42+
const match: RegExpMatchArray | null = imageName.match(/^(?:(?<registryName>[^/]+)\/)?(?:(?<namespace>[^/]+(?:\/[^/]+)*)\/)?(?<repositoryName>[^/:]+)(?::(?<tag>[^/]+))?$/);
43+
return {
44+
imageNameReference: imageName,
45+
registryDomain: match?.groups?.registryName ? detectRegistryDomain(match.groups.registryName) : undefined,
46+
registryName: match?.groups?.registryName,
47+
namespace: match?.groups?.namespace,
48+
repositoryName: match?.groups?.repositoryName,
49+
tag: match?.groups?.tag
50+
};
51+
}
52+
1253
/**
1354
* @param registryName When parsed from a full image name, everything before the first slash
1455
*/

0 commit comments

Comments
 (0)