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

Add an Edit Container Image... command #778

Merged
merged 20 commits into from
Dec 2, 2024
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
11 changes: 11 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,12 @@
"title": "%containerApps.editContainer%",
"category": "Azure Container Apps"
},
{
"command": "containerApps.editContainerImage",
"title": "%containerApps.editContainerImage.title%",
"shortTitle": "%containerApps.editContainerImage.shortTitle%",
"category": "Azure Container Apps"
},
{
"command": "containerApps.editScaleRange",
"title": "%containerApps.editScaleRange%",
Expand Down Expand Up @@ -463,6 +469,11 @@
"when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /containerItem/i",
"group": "1@1"
},
{
"command": "containerApps.editContainerImage",
"when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /imageItem/i",
"group": "1@1"
},
{
"command": "containerApps.editScaleRange",
"when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /scaleItem/i",
Expand Down
2 changes: 2 additions & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
"containerApps.createContainerAppFromWorkspace": "Create Container App from Workspace...",
"containerApps.editContainerApp": "Edit Container App (Advanced)...",
"containerApps.editContainer": "Edit Container...",
"containerApps.editContainerImage.title": "Edit Container Image...",
"containerApps.editContainerImage.shortTitle": "Edit Image...",
"containerApps.deployImageApi": "Deploy Image to Container App (API)...",
"containerApps.deployWorkspaceProject": "Deploy Project from Workspace...",
"containerApps.deployWorkspaceProjectApi": "Deploy Project from Workspace (API)...",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { type Container, type Revision } from "@azure/arm-appcontainers";
import { activityFailContext, activityFailIcon, activityProgressContext, activityProgressIcon, activitySuccessContext, activitySuccessIcon, createUniversallyUniqueContextValue, GenericParentTreeItem, GenericTreeItem, nonNullProp, type ExecuteActivityOutput } from "@microsoft/vscode-azext-utils";
import { type Progress } from "vscode";
import { type ContainerAppItem, type ContainerAppModel } from "../../../tree/ContainerAppItem";
import { type RevisionsItemModel } from "../../../tree/revisionManagement/RevisionItem";
import { localize } from "../../../utils/localize";
import { getParentResourceFromItem } from "../../../utils/revisionDraftUtils";
import { getContainerNameForImage } from "../../image/imageSource/containerRegistry/getContainerNameForImage";
import { RevisionDraftUpdateBaseStep } from "../../revisionDraft/RevisionDraftUpdateBaseStep";
import { type ContainerEditUpdateContext } from "./editContainerImage";

export class ContainerImageEditDraftStep<T extends ContainerEditUpdateContext> extends RevisionDraftUpdateBaseStep<T> {
public priority: number = 590;

constructor(baseItem: ContainerAppItem | RevisionsItemModel) {
super(baseItem);
}

public async execute(context: T, progress: Progress<{ message?: string | undefined; increment?: number | undefined }>): Promise<void> {
progress.report({ message: localize('editingImage', 'Editing image (draft)...') });
this.revisionDraftTemplate.containers ??= [];

const container: Container = this.revisionDraftTemplate.containers[context.containersIdx] ?? {};
container.name = getContainerNameForImage(nonNullProp(context, 'image'));
container.image = context.image;

await this.updateRevisionDraftWithTemplate(context);
}

public shouldExecute(context: T): boolean {
return context.containersIdx !== undefined && !!context.image;
}

public createSuccessOutput(context: T): ExecuteActivityOutput {
const parentResource: ContainerAppModel | Revision = getParentResourceFromItem(this.baseItem);
return {
item: new GenericTreeItem(undefined, {
contextValue: createUniversallyUniqueContextValue(['containerImageEditDraftStepSuccessItem', activitySuccessContext]),
label: localize('editImage', 'Edit container image for app "{0}" (draft)', parentResource.name),
iconPath: activitySuccessIcon,
}),
message: localize('editImageSuccess', 'Successfully added image "{0}" to container app "{1}" (draft).', context.image, parentResource.name),
};
}

public createProgressOutput(): ExecuteActivityOutput {
const parentResource: ContainerAppModel | Revision = getParentResourceFromItem(this.baseItem);
return {
item: new GenericTreeItem(undefined, {
contextValue: createUniversallyUniqueContextValue(['containerImageEditDraftStepProgressItem', activityProgressContext]),
label: localize('editImage', 'Edit container image for app "{0}" (draft)', parentResource.name),
iconPath: activityProgressIcon,
}),
};
}

public createFailOutput(context: T): ExecuteActivityOutput {
const parentResource: ContainerAppModel | Revision = getParentResourceFromItem(this.baseItem);
return {
item: new GenericParentTreeItem(undefined, {
contextValue: createUniversallyUniqueContextValue(['containerImageEditDraftStepFailItem', activityFailContext]),
label: localize('editImage', 'Edit container image for app "{0}" (draft)', parentResource.name),
iconPath: activityFailIcon,
}),
message: localize('editImageFail', 'Failed to add image "{0}" to container app "{1}" (draft).', context.image, parentResource.name),
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.md in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { type Revision } from "@azure/arm-appcontainers";
import { AzureWizard, createSubscriptionContext, type IActionContext, type ISubscriptionContext } from "@microsoft/vscode-azext-utils";
import { type ContainerAppModel } from "../../../tree/ContainerAppItem";
import { type ImageItem } from "../../../tree/containers/ImageItem";
import { createActivityContext } from "../../../utils/activityUtils";
import { getManagedEnvironmentFromContainerApp } from "../../../utils/getResourceUtils";
import { getVerifyProvidersStep } from "../../../utils/getVerifyProvidersStep";
import { localize } from "../../../utils/localize";
import { pickImage } from "../../../utils/pickItem/pickImage";
import { getParentResourceFromItem, isTemplateItemEditable, TemplateItemNotEditableError } from "../../../utils/revisionDraftUtils";
import { ImageSourceListStep } from "../../image/imageSource/ImageSourceListStep";
import { RevisionDraftDeployPromptStep } from "../../revisionDraft/RevisionDraftDeployPromptStep";
import { type ContainerEditContext } from "../ContainerEditContext";
import { RegistryAndSecretsUpdateStep } from "../RegistryAndSecretsUpdateStep";
import { ContainerImageEditDraftStep } from "./ContainerImageEditDraftStep";

export type ContainerEditUpdateContext = ContainerEditContext;

// Edits only the 'image' portion of the container profile
export async function editContainerImage(context: IActionContext, node?: ImageItem): Promise<void> {
const item: ImageItem = node ?? await pickImage(context, { autoSelectDraft: true });
const { subscription, containerApp } = item;

if (!isTemplateItemEditable(item)) {
throw new TemplateItemNotEditableError(item);
}

const subscriptionContext: ISubscriptionContext = createSubscriptionContext(subscription);
const parentResource: ContainerAppModel | Revision = getParentResourceFromItem(item);

const wizardContext: ContainerEditUpdateContext = {
...context,
...subscriptionContext,
...await createActivityContext(true),
subscription,
managedEnvironment: await getManagedEnvironmentFromContainerApp({ ...context, ...subscriptionContext }, containerApp),
containerApp,
containersIdx: item.containersIdx,
};
wizardContext.telemetry.properties.revisionMode = containerApp.revisionsMode;

const wizard: AzureWizard<ContainerEditUpdateContext> = new AzureWizard(wizardContext, {
title: localize('editContainerImage', 'Edit container image for app "{0}" (draft)', parentResource.name),
promptSteps: [
new ImageSourceListStep({ suppressEnvPrompt: true }),
new RevisionDraftDeployPromptStep(),
],
executeSteps: [
getVerifyProvidersStep<ContainerEditUpdateContext>(),
new RegistryAndSecretsUpdateStep(),
new ContainerImageEditDraftStep(item),
],
showLoadingPrompt: true,
});

await wizard.prompt();
await wizard.execute();
}
12 changes: 11 additions & 1 deletion src/commands/image/imageSource/ImageSourceListStep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,15 @@ import { ContainerRegistryImageConfigureStep } from "./containerRegistry/Contain
import { ContainerRegistryListStep } from "./containerRegistry/ContainerRegistryListStep";
import { AcrListStep } from "./containerRegistry/acr/AcrListStep";

interface ImageSourceListStepOptions {
suppressEnvPrompt?: boolean;
}

export class ImageSourceListStep extends AzureWizardPromptStep<ImageSourceContext> {
constructor(private readonly options?: ImageSourceListStepOptions) {
super();
}

public async prompt(context: ImageSourceContext): Promise<void> {
const imageSourceLabels: string[] = [
localize('containerRegistryLabel', 'Container Registry'),
Expand Down Expand Up @@ -81,7 +89,9 @@ export class ImageSourceListStep extends AzureWizardPromptStep<ImageSourceContex
default:
}

promptSteps.push(new EnvironmentVariablesListStep());
if (!this.options?.suppressEnvPrompt) {
promptSteps.push(new EnvironmentVariablesListStep());
}

return { promptSteps, executeSteps };
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import { getLoginServer } from "./getLoginServer";
export class ContainerRegistryImageConfigureStep<T extends ContainerRegistryImageSourceContext> extends AzureWizardActivityOutputExecuteStep<T> {
public priority: number = 570;
public stepName: string = 'containerRegistryImageConfigureStep';
protected getSuccessString = (context: T) => localize('successOutput', 'Successfully set container app image to "{0}".', context.image);
protected getFailString = (context: T) => localize('failOutput', 'Failed to set container app image to "{0}".', context.image);
protected getTreeItemLabel = (context: T) => localize('treeItemLabel', 'Set container app image to "{0}"', context.image);
protected getSuccessString = (context: T) => localize('successOutput', 'Successfully set container image to "{0}".', context.image);
protected getFailString = (context: T) => localize('failOutput', 'Failed to set container image to "{0}".', context.image);
protected getTreeItemLabel = (context: T) => localize('treeItemLabel', 'Set container image to "{0}"', context.image);

public async execute(context: T): Promise<void> {
context.image = `${getLoginServer(context)}/${context.repositoryName}:${context.tag}`;
Expand Down
2 changes: 2 additions & 0 deletions src/commands/registerCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { deleteContainerApp } from './deleteContainerApp/deleteContainerApp';
import { deleteManagedEnvironment } from './deleteManagedEnvironment/deleteManagedEnvironment';
import { deployWorkspaceProject } from './deployWorkspaceProject/deployWorkspaceProject';
import { editContainer } from './editContainer/editContainer';
import { editContainerImage } from './editContainer/editContainerImage/editContainerImage';
import { editContainerApp } from './editContainerApp';
import { connectToGitHub } from './gitHub/connectToGitHub/connectToGitHub';
import { disconnectRepo } from './gitHub/disconnectRepo/disconnectRepo';
Expand Down Expand Up @@ -65,6 +66,7 @@ export function registerCommands(): void {

// containers
registerCommandWithTreeNodeUnwrapping('containerApps.editContainer', editContainer);
registerCommandWithTreeNodeUnwrapping('containerApps.editContainerImage', editContainerImage);

// deploy
registerCommandWithTreeNodeUnwrapping('containerApps.deployImageApi', deployImageApi);
Expand Down
10 changes: 5 additions & 5 deletions src/tree/containers/ImageItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,11 @@ export class ImageItem extends RevisionDraftDescendantBase {
}

const currentContainers: Container[] = this.parentResource.template?.containers ?? [];
const currentContainer: Container = currentContainers[this.containersIdx];
const currentContainer: Container | undefined = currentContainers[this.containersIdx];

return {
imageNameItem: this.getImageName(currentContainer.image) !== this.getImageName(this.container.image),
imageRegistryItem: this.getLoginServer(currentContainer.image) !== this.getLoginServer(this.container.image),
imageNameItem: this.getImageName(currentContainer?.image) !== this.getImageName(this.container.image),
imageRegistryItem: this.getLoginServer(currentContainer?.image) !== this.getLoginServer(this.container.image),
};
}

Expand All @@ -111,8 +111,8 @@ export class ImageItem extends RevisionDraftDescendantBase {
}

const currentContainers: Container[] = this.parentResource.template?.containers ?? [];
const currentContainer: Container = currentContainers[this.containersIdx];
const currentContainer: Container | undefined = currentContainers[this.containersIdx];

return this.container.image !== currentContainer.image;
return this.container.image !== currentContainer?.image;
}
}
4 changes: 2 additions & 2 deletions src/tree/revisionManagement/RevisionDraftItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export interface RevisionsDraftModel {
}

export class RevisionDraftItem implements RevisionsItemModel, RevisionsDraftModel {
static readonly idSuffix: string = '/revisionDraft';
static readonly idSuffix: string = 'revisionDraft';
static readonly contextValue: string = 'revisionDraftItem';
static readonly contextValueRegExp: RegExp = new RegExp(RevisionDraftItem.contextValue);

Expand Down Expand Up @@ -58,7 +58,7 @@ export class RevisionDraftItem implements RevisionsItemModel, RevisionsDraftMode

static isRevisionDraftItem(item: unknown): item is RevisionDraftItem {
return typeof item === 'object' &&
(item as RevisionDraftItem).id === 'string' &&
typeof (item as RevisionDraftItem).id === 'string' &&
(item as RevisionDraftItem).id.split('/').at(-1) === RevisionDraftItem.idSuffix;
}

Expand Down
53 changes: 53 additions & 0 deletions src/utils/pickItem/parentResourcePickSteps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { KnownActiveRevisionsMode } from "@azure/arm-appcontainers";
import { AzureWizardPromptStep, type AzureResourceQuickPickWizardContext, type IWizardOptions, type QuickPickWizardContext } from "@microsoft/vscode-azext-utils";
import { type ResourceModelBase } from "@microsoft/vscode-azureresources-api";
import { ext } from "../../extensionVariables";
import { ContainerAppItem } from "../../tree/ContainerAppItem";
import { localize } from "../localize";
import { type RevisionDraftPickItemOptions } from "./PickItemOptions";
import { getPickRevisionDraftStep, getPickRevisionsStep, getPickRevisionStep } from "./pickRevision";

/**
* Use to add pick steps that automatically select down to the appropriate parent resource (`ContainerAppItem`, `RevisionItem`, or `RevisionDraftItem`)
* given that the last node picked was a `ContainerAppItem`.
*/
export class ParentResourceItemPickSteps<T extends AzureResourceQuickPickWizardContext> extends AzureWizardPromptStep<T> {
constructor(readonly options?: RevisionDraftPickItemOptions) {
super();
}

public async prompt(): Promise<void> {
// Nothing to prompt, just need to use the subwizard
}

public shouldPrompt(): boolean {
return false;
}

public async getSubWizard(context: T): Promise<IWizardOptions<T> | undefined> {
const lastNode: unknown = context.pickedNodes.at(-1);
const containerAppItem: unknown = (lastNode as { branchItem?: ResourceModelBase })?.branchItem ?? lastNode;

if (!ContainerAppItem.isContainerAppItem(containerAppItem)) {
throw new Error(localize('expectedContainerAppItem', 'Internal error: Expected last picked item to be a "ContainerAppItem".'));
}

const promptSteps: AzureWizardPromptStep<QuickPickWizardContext>[] = [];
if (containerAppItem.containerApp.revisionsMode === KnownActiveRevisionsMode.Multiple) {
promptSteps.push(getPickRevisionsStep());

if (this.options?.autoSelectDraft && ext.revisionDraftFileSystem.doesContainerAppsItemHaveRevisionDraft(containerAppItem)) {
promptSteps.push(getPickRevisionDraftStep());
} else {
promptSteps.push(getPickRevisionStep());
}
}

return { promptSteps };
}
}
Loading