Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/commands/createContainerApp/setQuickStartImage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<IContainerAppContext>): void {
context.image = 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest';
context.image = quickStartImageName;
context.enableIngress = true;
context.enableExternal = true;
context.targetPort = 80;
Expand Down
31 changes: 29 additions & 2 deletions src/commands/imageSource/containerRegistry/acr/AcrListStep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<IContainerRegistryImageContext> {
Expand All @@ -32,8 +36,31 @@ export class AcrListStep extends AzureWizardPromptStep<IContainerRegistryImageCo

public async getPicks(context: IContainerRegistryImageContext): Promise<IAzureQuickPickItem<Registry>[]> {
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 };
});
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -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<QuickPickItem[]> {
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 };
});
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
3 changes: 3 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export namespace RevisionConstants {
export const single: IAzureQuickPickItem<string> = { 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"
Expand Down Expand Up @@ -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';

Expand Down
43 changes: 42 additions & 1 deletion src/utils/imageNameUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: '<registryName>/<...namespaces>/<repositoryName>:<tag>'
* 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(/^(?:(?<registryName>[^/]+)\/)?(?:(?<namespace>[^/]+(?:\/[^/]+)*)\/)?(?<repositoryName>[^/:]+)(?::(?<tag>[^/]+))?$/);
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
*/
Expand Down