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
214 changes: 199 additions & 15 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,7 @@
"dependencies": {
"@azure/arm-appcontainers": "^1.1.0",
"@azure/arm-containerregistry": "^10.0.0",
"@azure/storage-blob": "^12.4.1",
"@azure/arm-operationalinsights": "^8.0.0",
"@azure/arm-resources": "^4.2.2",
"@azure/container-registry": "1.0.0-beta.5",
Expand All @@ -341,7 +342,8 @@
"dotenv": "^16.0.0",
"open": "^8.0.4",
"semver": "^7.3.5",
"vscode-nls": "^4.1.1"
"vscode-nls": "^4.1.1",
"vscode-uri": "^3.0.2"
},
"extensionDependencies": [
"ms-vscode.azure-account",
Expand Down
3 changes: 2 additions & 1 deletion src/commands/deploy/IDeployBaseContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@
import type { EnvironmentVar, RegistryCredentials, Secret } from "@azure/arm-appcontainers";
import { ISubscriptionActionContext } from "@microsoft/vscode-azext-utils";
import { AzureSubscription } from "@microsoft/vscode-azureresources-api";
import { ImageSourceValues } from "../../constants";
import { ImageSource, ImageSourceValues } from "../../constants";
import { ContainerAppModel } from "../../tree/ContainerAppItem";

export interface IDeployBaseContext extends ISubscriptionActionContext {
subscription: AzureSubscription;
targetContainer?: ContainerAppModel;

imageSource?: ImageSourceValues;
buildType?: ImageSource.LocalDockerBuild | ImageSource.RemoteAcrBuild;
showQuickStartImage?: boolean;

// Base image attributes used as a precursor for either creating or updating a container app
Expand Down
9 changes: 7 additions & 2 deletions src/commands/deploy/ImageSourceListStep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { localize } from "../../utils/localize";
import { setQuickStartImage } from "../createContainerApp/setQuickStartImage";
import { EnvironmentVariablesListStep } from "./EnvironmentVariablesListStep";
import { IDeployBaseContext } from "./IDeployBaseContext";
import { BuildFromProjectListStep } from "./buildImageInAzure/BuildFromProjectListStep";
import { ContainerRegistryListStep } from "./deployFromRegistry/ContainerRegistryListStep";
import { DeployFromRegistryConfigureStep } from "./deployFromRegistry/DeployFromRegistryConfigureStep";

Expand All @@ -17,13 +18,13 @@ export class ImageSourceListStep extends AzureWizardPromptStep<IDeployBaseContex
const imageSourceLabels: string[] = [
localize('externalRegistry', 'Use existing image'),
localize('quickStartImage', 'Use quickstart image'),
// localize('buildFromProject', 'Build from project'),
localize('buildFromProject', 'Build from project remotely using Azure Container Registry'),
];

const placeHolder: string = localize('imageBuildSourcePrompt', 'Select an image source for the container app');
const picks: IAzureQuickPickItem<ImageSourceValues | undefined>[] = [
{ label: imageSourceLabels[0], data: ImageSource.ExternalRegistry, suppressPersistence: true },
// { label: imageSourceLabels[2], data: undefined, suppressPersistence: true },
{ label: imageSourceLabels[2], data: ImageSource.RemoteAcrBuild, suppressPersistence: true },
];

if (context.showQuickStartImage) {
Expand All @@ -49,6 +50,10 @@ export class ImageSourceListStep extends AzureWizardPromptStep<IDeployBaseContex
promptSteps.push(new ContainerRegistryListStep());
executeSteps.push(new DeployFromRegistryConfigureStep());
break;
case ImageSource.RemoteAcrBuild:
promptSteps.push(new BuildFromProjectListStep());
executeSteps.push(new DeployFromRegistryConfigureStep());
break;
default:
// Todo: Steps that lead to additional 'Build from project' options
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.md in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { AzureWizardExecuteStep, AzureWizardPromptStep, IAzureQuickPickItem, IWizardOptions } from "@microsoft/vscode-azext-utils";
import { ImageSource } from "../../../constants";
import { localize } from "../../../utils/localize";
import { IDeployBaseContext } from "../IDeployBaseContext";
import { AcrListStep } from "../deployFromRegistry/acr/AcrListStep";
import { BuildImageStep } from "./BuildImageStep";
import { DockerFileItemStep } from "./DockerFileItemStep";
import { IBuildImageInAzureContext } from "./IBuildImageInAzureContext";
import { ImageNameStep } from "./ImageNameStep";
import { OSPickStep } from "./OSPickStep";
import { RootFolderStep } from "./RootFolderStep";
import { RunStep } from "./RunStep";
import { TarFileStep } from "./TarFileStep";
import { UploadSourceCodeStep } from "./UploadSourceCodeStep";

const buildFromProjectLabels: string[] = [
localize('azure', 'Build from project remotely using Azure Container Registry')
//localize('docker', 'Build from project locally using Docker')
Comment thread
alexweininger marked this conversation as resolved.
];

export class BuildFromProjectListStep extends AzureWizardPromptStep<IDeployBaseContext> {
public async prompt(context: IDeployBaseContext): Promise<void> {
const placeHolder: string = localize('buildType', 'Select how you want to build your project');
const picks: IAzureQuickPickItem<ImageSource.LocalDockerBuild | ImageSource.RemoteAcrBuild>[] = [
{ label: buildFromProjectLabels[0], data: ImageSource.RemoteAcrBuild, suppressPersistence: true },
//{ label: buildFromProjectLabels[1], data: ImageSource.LocalDockerBuild, suppressPersistence: true }
];

context.buildType = (await context.ui.showQuickPick(picks, { placeHolder })).data;
}

public async configureBeforePrompt(context: IDeployBaseContext): Promise<void> {
if (buildFromProjectLabels.length === 1) {
context.buildType = ImageSource.RemoteAcrBuild;
}
}

public shouldPrompt(context: IDeployBaseContext): boolean {
return !context.buildType;
}

public async getSubWizard(context: IBuildImageInAzureContext): Promise<IWizardOptions<IDeployBaseContext> | undefined> {
const promptSteps: AzureWizardPromptStep<IDeployBaseContext>[] = [];
const executeSteps: AzureWizardExecuteStep<IDeployBaseContext>[] = [];

switch (context.buildType) {
case ImageSource.RemoteAcrBuild:
promptSteps.push(new AcrListStep(), new RootFolderStep(), new DockerFileItemStep(), new ImageNameStep(), new OSPickStep());
executeSteps.push(new TarFileStep(), new UploadSourceCodeStep(), new RunStep(), new BuildImageStep());
break;
//TODO: case for 'Build from project locally using Docker'
}
return { promptSteps, executeSteps };
}
}
33 changes: 33 additions & 0 deletions src/commands/deploy/buildImageInAzure/BuildImageStep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.md in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { AzureWizardExecuteStep } from "@microsoft/vscode-azext-utils";
import { acrDomain } from "../../../constants";
import { localize } from "../../../utils/localize";
import { IBuildImageInAzureContext } from "./IBuildImageInAzureContext";
import { buildImageInAzure } from "./buildImageInAzure";

export class BuildImageStep extends AzureWizardExecuteStep<IBuildImageInAzureContext> {
public priority: number = 225;

public async execute(context: IBuildImageInAzureContext): Promise<void> {
context.registryDomain = acrDomain;

const run = await buildImageInAzure(context);
const outputImages = run?.outputImages;
context.telemetry.properties.outputImages = outputImages?.length?.toString();

if (outputImages) {
Comment thread
bwateratmsft marked this conversation as resolved.
const image = outputImages[0];
context.image = `${image.registry}/${image.repository}:${image.tag}`;
} else {
throw new Error(localize('noImagesBuilt', 'Failed to build image.'));
}
}

public shouldExecute(context: IBuildImageInAzureContext): boolean {
return !context.image;
}
}
20 changes: 20 additions & 0 deletions src/commands/deploy/buildImageInAzure/DockerFileItemStep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.md in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { AzureWizardPromptStep } from '@microsoft/vscode-azext-utils';
import { DOCKERFILE_GLOB_PATTERN } from "../../../constants";
import { localize } from '../../../utils/localize';
import { selectWorkspaceFile } from "../../../utils/workspaceUtils";
import { IBuildImageInAzureContext } from "./IBuildImageInAzureContext";

export class DockerFileItemStep extends AzureWizardPromptStep<IBuildImageInAzureContext> {
public async prompt(context: IBuildImageInAzureContext): Promise<void> {
context.dockerFilePath = await selectWorkspaceFile(context, localize('dockerFilePick', 'Select a Dockerfile'), { filters: { 'Dockerfile': ['Dockerfile', 'Dockerfile.*'] } }, DOCKERFILE_GLOB_PATTERN);
}

public shouldPrompt(context: IBuildImageInAzureContext): boolean {
return !context.dockerFilePath;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.md in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import type { Run as AcrRun, ContainerRegistryManagementClient } from '@azure/arm-containerregistry';
import * as vscode from 'vscode';
import { IDeployFromRegistryContext } from '../deployFromRegistry/IDeployFromRegistryContext';

export interface IBuildImageInAzureContext extends IDeployFromRegistryContext {
rootFolder: vscode.WorkspaceFolder;
dockerFilePath: string;
imageName: string;
os: 'Windows' | 'Linux';

uploadedSourceLocation: string;
tarFilePath: string;

client: ContainerRegistryManagementClient;
resourceGroupName: string;
registryName: string;
run: AcrRun
}
37 changes: 37 additions & 0 deletions src/commands/deploy/buildImageInAzure/ImageNameStep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.md in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { AzureWizardPromptStep } from "@microsoft/vscode-azext-utils";
import { URI, Utils } from "vscode-uri";
import { localize } from "../../../utils/localize";
import { IBuildImageInAzureContext } from "./IBuildImageInAzureContext";

export class ImageNameStep extends AzureWizardPromptStep<IBuildImageInAzureContext> {
public async prompt(context: IBuildImageInAzureContext): Promise<void> {
const suggestedImageName = await getSuggestedName(context, context.dockerFilePath);

context.imageName = await context.ui.showInputBox({
prompt: localize('imageNamePrompt', 'Enter a name for the image'),
value: suggestedImageName ? localize('dockerfilePlaceholder', suggestedImageName) : ''
});
}

public shouldPrompt(context: IBuildImageInAzureContext): boolean {
return !context.imageName;
}

}

async function getSuggestedName(context: IBuildImageInAzureContext, dockerFilePath: string): Promise<string | undefined> {
let suggestedImageName: string | undefined;
suggestedImageName = Utils.dirname(URI.parse(dockerFilePath)).path.split('/').pop();
if (suggestedImageName === '') {
if (context.rootFolder) {
suggestedImageName = Utils.basename(context.rootFolder.uri).toLowerCase().replace(/\s/g, '');
}
}
suggestedImageName += ":{{.Run.ID}}";
return suggestedImageName;
}
24 changes: 24 additions & 0 deletions src/commands/deploy/buildImageInAzure/OSPickStep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.md in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { AzureWizardPromptStep, IAzureQuickPickItem } from "@microsoft/vscode-azext-utils";
import { localize } from "../../../utils/localize";
import { IBuildImageInAzureContext } from "./IBuildImageInAzureContext";

export class OSPickStep extends AzureWizardPromptStep<IBuildImageInAzureContext> {
public async prompt(context: IBuildImageInAzureContext): Promise<void> {
const placeHolder: string = localize('imageOSPrompt', 'Select image base OS');
const picks: IAzureQuickPickItem<'Windows' | 'Linux'>[] = [
{ label: 'Linux', data: 'Linux', suppressPersistence: true },
{ label: 'Windows', data: 'Windows', suppressPersistence: true },
];

context.os = (await context.ui.showQuickPick(picks, { placeHolder })).data;
}

public shouldPrompt(context: IBuildImageInAzureContext): boolean {
return !context.os;
}
}
33 changes: 33 additions & 0 deletions src/commands/deploy/buildImageInAzure/RootFolderStep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.md in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { AzureWizardPromptStep, UserCancelledError } from '@microsoft/vscode-azext-utils';
import * as vscode from 'vscode';
import { localize } from '../../../utils/localize';
import { IBuildImageInAzureContext } from './IBuildImageInAzureContext';

export class RootFolderStep extends AzureWizardPromptStep<IBuildImageInAzureContext> {
public async prompt(context: IBuildImageInAzureContext): Promise<void> {
context.rootFolder = await getRootWorkSpaceFolder();
}

public shouldPrompt(context: IBuildImageInAzureContext): boolean {
return !context.rootFolder;
}
}

async function getRootWorkSpaceFolder(): Promise<vscode.WorkspaceFolder> {
if (!vscode.workspace.workspaceFolders || vscode.workspace.workspaceFolders.length === 0) {
throw new Error(localize('noOpenFolder', 'No folder is open. Please open a folder and try again.'));
} else if (vscode.workspace.workspaceFolders.length === 1) {
return vscode.workspace.workspaceFolders[0];
} else {
const placeHolder: string = localize('selectRootWorkspace', 'Select the folder containing your Dockerfile');
const folder = await vscode.window.showWorkspaceFolderPick({ placeHolder });
if (!folder) {
throw new UserCancelledError('selectRootWorkspace');
}
return folder;
}
}
43 changes: 43 additions & 0 deletions src/commands/deploy/buildImageInAzure/RunStep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.md in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import type { DockerBuildRequest as AcrDockerBuildRequest } from "@azure/arm-containerregistry";
import { AzExtFsExtra, AzureWizardExecuteStep } from "@microsoft/vscode-azext-utils";
import * as path from 'path';
import { Progress } from "vscode";
import { localize } from "../../../utils/localize";
import { IBuildImageInAzureContext } from "./IBuildImageInAzureContext";

export class RunStep extends AzureWizardExecuteStep<IBuildImageInAzureContext> {
public priority: number = 200;

public async execute(context: IBuildImageInAzureContext, progress: Progress<{ message?: string | undefined; increment?: number | undefined }>): Promise<void> {
try {
const rootUri = context.rootFolder.uri;

const runRequest: AcrDockerBuildRequest = {
type: 'DockerBuildRequest',
imageNames: [context.imageName],
isPushEnabled: true,
sourceLocation: context.uploadedSourceLocation,
platform: { os: context.os },
dockerFilePath: path.relative(rootUri.path, context.dockerFilePath)
};

const building: string = localize('buildingImage', 'Building image "{0}" in registry "{1}"...', context.imageName, context.registryName);
progress.report({ message: building });

context.run = await context.client.registries.beginScheduleRunAndWait(context.resourceGroupName, context.registryName, runRequest);
} finally {
if (await AzExtFsExtra.pathExists(context.tarFilePath)) {
await AzExtFsExtra.deleteResource(context.tarFilePath);
}
}
}

public shouldExecute(context: IBuildImageInAzureContext): boolean {
return !context.run
}
}
25 changes: 25 additions & 0 deletions src/commands/deploy/buildImageInAzure/TarFileStep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.md in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { AzureWizardExecuteStep } from "@microsoft/vscode-azext-utils";
import * as os from 'os';
import { URI, Utils } from "vscode-uri";
import { IBuildImageInAzureContext } from "./IBuildImageInAzureContext";

const idPrecision = 6;

export class TarFileStep extends AzureWizardExecuteStep<IBuildImageInAzureContext> {
public priority: number = 150;

public async execute(context: IBuildImageInAzureContext): Promise<void> {
const id: number = Math.floor(Math.random() * Math.pow(10, idPrecision));
const archive = `sourceArchive${id}.tar.gz`;
context.tarFilePath = Utils.joinPath(URI.parse(os.tmpdir()), archive).path;
}

public shouldExecute(context: IBuildImageInAzureContext): boolean {
return !context.tarFilePath;
}
}
Loading