Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support deployment to Azure Container Apps with ACA extension #3794

Merged
merged 19 commits into from
Feb 6, 2023
Merged
Show file tree
Hide file tree
Changes from 17 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
14 changes: 13 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@
"onCommand:vscode-docker.registries.copyRemoteFullTag",
"onCommand:vscode-docker.registries.deleteImage",
"onCommand:vscode-docker.registries.deployImageToAzure",
"onCommand:vscode-docker.registries.deployImageToAca",
"onCommand:vscode-docker.registries.disconnectRegistry",
"onCommand:vscode-docker.registries.dockerHub.openInBrowser",
"onCommand:vscode-docker.registries.help",
Expand Down Expand Up @@ -584,6 +585,11 @@
"when": "view == dockerRegistries && viewItem =~ /(DockerV2|DockerHubV2);Tag;/ && isAzureAccountInstalled",
"group": "regs_tag_1_general@4"
},
{
"command": "vscode-docker.registries.deployImageToAca",
"when": "view == dockerRegistries && viewItem =~ /(DockerV2|DockerHubV2|GitLabV4);Tag;/ && isAzureAccountInstalled",
"group": "regs_tag_1_general@6"
},
{
"command": "vscode-docker.registries.azure.untagImage",
"when": "view == dockerRegistries && viewItem == azure;DockerV2;Tag;",
Expand Down Expand Up @@ -2594,6 +2600,11 @@
"title": "%vscode-docker.commands.registries.deployImageToAzure%",
"category": "%vscode-docker.commands.category.dockerRegistries%"
},
{
"command": "vscode-docker.registries.deployImageToAca",
"title": "%vscode-docker.commands.registries.deployImageToAca%",
"category": "%vscode-docker.commands.category.dockerRegistries%"
},
{
"command": "vscode-docker.registries.disconnectRegistry",
"title": "%vscode-docker.commands.registries.disconnectRegistry%",
Expand Down Expand Up @@ -2869,7 +2880,8 @@
"id": "azDeploy",
"title": "%vscode-docker.walkthrough.dockerStart.azDeploy.title%",
"completionEvents": [
"onCommand:vscode-docker.registries.deployImageToAzure"
"onCommand:vscode-docker.registries.deployImageToAzure",
"onCommand:vscode-docker.registries.deployImageToAca"
],
"description": "%vscode-docker.walkthrough.dockerStart.azDeploy.description%",
"when": "isAzureAccountInstalled",
Expand Down
1 change: 1 addition & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@
"vscode-docker.commands.registries.copyRemoteFullTag": "Copy Full Tag",
"vscode-docker.commands.registries.deleteImage": "Delete Image...",
"vscode-docker.commands.registries.deployImageToAzure": "Deploy Image to Azure App Service...",
"vscode-docker.commands.registries.deployImageToAca": "Deploy Image to Azure Container Apps...",
"vscode-docker.commands.registries.disconnectRegistry": "Disconnect",
"vscode-docker.commands.registries.dockerHub.openInBrowser": "Open in Browser",
"vscode-docker.commands.registries.help": "Registries Help",
Expand Down
2 changes: 2 additions & 0 deletions src/commands/registerCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import { registerWorkspaceCommand } from "./registerWorkspaceCommand";
import { createAzureRegistry } from "./registries/azure/createAzureRegistry";
import { deleteAzureRegistry } from "./registries/azure/deleteAzureRegistry";
import { deleteAzureRepository } from "./registries/azure/deleteAzureRepository";
import { deployImageToAca } from "./registries/azure/deployImageToAca";
import { deployImageToAzure } from "./registries/azure/deployImageToAzure";
import { openInAzurePortal } from "./registries/azure/openInAzurePortal";
import { buildImageInAzure } from "./registries/azure/tasks/buildImageInAzure";
Expand Down Expand Up @@ -167,6 +168,7 @@ export function registerCommands(): void {
registerCommand('vscode-docker.registries.copyRemoteFullTag', copyRemoteFullTag);
registerCommand('vscode-docker.registries.deleteImage', deleteRemoteImage);
registerCommand('vscode-docker.registries.deployImageToAzure', deployImageToAzure);
registerCommand('vscode-docker.registries.deployImageToAca', deployImageToAca);
registerCommand('vscode-docker.registries.disconnectRegistry', disconnectRegistry);
registerCommand('vscode-docker.registries.help', registryHelp);
registerWorkspaceCommand('vscode-docker.registries.logInToDockerCli', logInToDockerCli);
Expand Down
109 changes: 109 additions & 0 deletions src/commands/registries/azure/deployImageToAca.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See LICENSE.md in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as semver from 'semver';
import * as vscode from 'vscode';
import { DialogResponses, IActionContext, nonNullProp, UserCancelledError } from '@microsoft/vscode-azext-utils';
import { RemoteTagTreeItem } from '../../../tree/registries/RemoteTagTreeItem';
import { localize } from '../../../localize';
import { ext } from '../../../extensionVariables';
import { registryExpectedContextValues } from '../../../tree/registries/registryContextValues';
import { RegistryTreeItemBase } from '../../../tree/registries/RegistryTreeItemBase';
import { AzureRegistryTreeItem } from '../../../tree/registries/azure/AzureRegistryTreeItem';
import { DockerHubNamespaceTreeItem } from '../../../tree/registries/dockerHub/DockerHubNamespaceTreeItem';
import { addImageTaggingTelemetry } from '../../images/tagImage';
import { parseDockerLikeImageName } from '../../../runtimes/docker/clients/DockerClientBase/parseDockerLikeImageName';

const acaExtensionId = 'ms-azuretools.vscode-azurecontainerapps';
const minimumAcaExtensionVersion = '0.4.0'; // TODO: get the exact minimum version that is needed
bwateratmsft marked this conversation as resolved.
Show resolved Hide resolved

// The interface of the command options passed to the Azure Container Apps extension's deployImageToAca command
interface DeployImageToAcaOptionsContract {
bwateratmsft marked this conversation as resolved.
Show resolved Hide resolved
image: string;
registryName: string;
username?: string;
secret?: string;
}

export async function deployImageToAca(context: IActionContext, node?: RemoteTagTreeItem): Promise<void> {
// Assert installation of the ACA extension
if (!isAcaExtensionInstalled()) {
// This will always throw a `UserCancelledError` but with the appropriate step name
// based on user choice about installation
await openAcaInstallPage(context);
}

if (!node) {
node = await ext.registriesTree.showTreeItemPicker<RemoteTagTreeItem>([registryExpectedContextValues.dockerHub.tag, registryExpectedContextValues.dockerV2.tag], context);
}

const commandOptions: Partial<DeployImageToAcaOptionsContract> = {
image: node.fullTag,
};

addImageTaggingTelemetry(context, commandOptions.image, '');

const registry: RegistryTreeItemBase = node.parent.parent;
if (registry instanceof AzureRegistryTreeItem) {
// No additional work to do; ACA can handle this on its own
} else {
const { auth } = await registry.getDockerCliCredentials() as { auth?: { username?: string, password?: string } };

if (!auth?.username || !auth?.password) {
throw new Error(localize('vscode-docker.commands.registries.azure.deployImageToAca.noCredentials', 'No credentials found for registry "{0}".', registry.label));
}

if (registry instanceof DockerHubNamespaceTreeItem) {
// Ensure Docker Hub images are prefixed with 'docker.io/...'
if (!/^docker\.io\//i.test(commandOptions.image)) {
bwateratmsft marked this conversation as resolved.
Show resolved Hide resolved
commandOptions.image = 'docker.io/' + commandOptions.image;
}
}

commandOptions.username = auth.username;
commandOptions.secret = auth.password;
}

commandOptions.registryName = nonNullProp(parseDockerLikeImageName(commandOptions.image), 'registry');

// Don't wait
void vscode.commands.executeCommand('containerApps.deployImageApi', commandOptions);
}

function isAcaExtensionInstalled(): boolean {
const acaExtension = vscode.extensions.getExtension(acaExtensionId);

if (!acaExtension?.packageJSON?.version) {
// If the ACA extension is not present, or the package JSON didn't come through, or the version is not present, then it's not installed
return false;
}

const acaVersion = semver.coerce(acaExtension.packageJSON.version);
const minVersion = semver.coerce(minimumAcaExtensionVersion);

return semver.gte(acaVersion, minVersion);
}

async function openAcaInstallPage(context: IActionContext): Promise<void> {
const message = localize(
'vscode-docker.commands.registries.azure.deployImageToAca.installAcaExtension',
'Version {0} or higher of the Azure Container Apps extension is required to deploy to Azure Container Apps. Would you like to install it now?',
minimumAcaExtensionVersion
);

const installButton: vscode.MessageItem = {
title: localize('vscode-docker.commands.registries.azure.deployImageToAca.install', 'Install'),
};

const response = await context.ui.showWarningMessage(message, { modal: true }, installButton, DialogResponses.cancel);

if (response !== installButton) {
throw new UserCancelledError('installAcaExtensionDeclined');
}

await vscode.commands.executeCommand('extension.open', acaExtensionId);

throw new UserCancelledError('installAcaExtensionOpened');
}
7 changes: 5 additions & 2 deletions src/scaffolding/copyWizardContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@ export function copyWizardContext(wizardContext: Partial<ScaffoldingWizardContex
}

for (const prop of Object.keys(priorWizardContext)) {
// Skip telemetry + error handling
if (prop === 'errorHandling' || prop === 'telemetry') {
// Skip properties from IActionContext
if (prop === 'telemetry' ||
prop === 'errorHandling' ||
prop === 'ui' ||
prop === 'valuesToMask') {
continue;
}

Expand Down