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
22 changes: 21 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@
"title": "%containerApps.editContainerApp%",
"category": "Azure Container Apps"
},
{
"command": "containerApps.updateImage",
"title": "%containerApps.updateImage%",
"category": "Azure Container Apps"
},
{
"command": "containerApps.deployImageApi",
"title": "%containerApps.deployImageApi%",
Expand Down Expand Up @@ -328,6 +333,11 @@
"when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /containerAppItem/i",
"group": "5@1"
},
{
"command": "containerApps.updateImage",
"when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /containerAppItem(.*)revisionMode:single/i",
"group": "6@1"
},
{
"command": "containerApps.startStreamingLogs",
"when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /containerAppItem/i",
Expand Down Expand Up @@ -368,6 +378,11 @@
"when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /revisionItem(.*)revisionState:active/i",
"group": "2@4"
},
{
"command": "containerApps.updateImage",
"when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /revisionDraft:false(.*)revisionItem/i",
"group": "3@1"
},
{
"command": "containerApps.deployRevisionDraft",
"when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /revisionDraftItem(.*)unsavedChanges:true/i",
Expand All @@ -389,10 +404,15 @@
"group": "1@2"
},
{
"command": "containerApps.editRevisionDraft",
"command": "containerApps.updateImage",
"when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /revisionDraftItem/i",
"group": "2@1"
},
{
"command": "containerApps.editRevisionDraft",
"when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /revisionDraftItem/i",
"group": "3@1"
},
{
"command": "containerApps.editScaleRange",
"when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /scaleItem/i",
Expand Down
1 change: 1 addition & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"containerApps.browse": "Browse",
"containerApps.createContainerApp": "Create Container App...",
"containerApps.editContainerApp": "Edit Container App (Advanced)...",
"containerApps.updateImage": "Update Container Image...",
"containerApps.deployImageApi": "Deploy Image to Container App (API)...",
"containerApps.deployWorkspaceProject": "Deploy Project from Workspace...",
"containerApps.deleteContainerApp": "Delete Container App...",
Expand Down
34 changes: 18 additions & 16 deletions src/commands/EXECUTE_PRIORITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,53 +22,56 @@ When creating or updating resources, execute steps should occupy certain priorit

- RegistryCreateStep: 350

### 3. Image Source
### 3. Image

<b>Priority Range</b>: 400 - 490

#### Build Image in Azure Steps
#### General Steps
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kind of find this hard to read:

image

I feel like a use of a divider or something could really help readability. I won't block for that, just wanted to point it out.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made an engineering issue

##### Build Image in Azure Steps

- TarFileStep: 420
- UploadSourceCodeStep: 430
- RunStep: 440
- BuildImageStep: 450
- ContainerRegistryImageConfigureStep: 470

#### Container Registry Steps
##### Container Registry Steps

- ContainerRegistryImageConfigureStep: 470

#### Common Steps
#### `updateImage` Steps

- ContainerAppUpdateStep: 480 (Todo - investigate decoupling this command from imageSource when revision draft update support is added)
- UpdateRegistryAndSecretsStep: 480
- UpdateImageDraftStep: 490 (revision draft)

### 4. Environment Variables
### 4. Unallocated Space

<b>Priority Range</b>: 500 - 590

#### Steps

Reserved

### 5. Ingress
### 5. Container App

<b>Priority Range</b>: 600 - 690

#### Steps

- EnableIngressStep: 650
- DisableIngressStep: 650
- ContainerAppCreateStep: 620
- ContainerAppUpdateStep: 650

- TargetPortUpdateStep: 650 (single command only)
- ToggleIngressVisibilityStep: 650 (single command only)

### 6. Container App
### 6. Ingress

<b>Priority Range</b>: 700 - 790

#### Steps

- ContainerAppCreateStep: 750
- EnableIngressStep: 750 (update existing container app)
- DisableIngressStep: 750 (update existing container app)

- TargetPortUpdateStep: 750 (single command only)
- ToggleIngressVisibilityStep: 750 (single command only)

### 7. Secrets

Expand All @@ -93,8 +96,7 @@ Reserved

#### Steps

- ScaleRangeUpdateStep: 1110
- AddScaleRuleStep: 1120
- AddScaleRuleStep: 1120 (revision draft)

### 10. Unallocated Space

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { getContainerNameForImage } from "../image/imageSource/containerRegistry
import type { ICreateContainerAppContext } from "./ICreateContainerAppContext";

export class ContainerAppCreateStep extends ExecuteActivityOutputStepBase<ICreateContainerAppContext> {
public priority: number = 750;
public priority: number = 620;

protected async executeCore(context: ICreateContainerAppContext, progress: Progress<{ message?: string | undefined; increment?: number | undefined }>): Promise<void> {
const appClient: ContainerAppsAPIClient = await createContainerAppsAPIClient(context);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export async function getAcrCredentialsAndSecrets(context: IContainerRegistryIma
const registries: RegistryCredentials[] = containerAppSettings?.registries?.filter(r => r.server !== registry.loginServer) ?? [];
registries?.push(
{
identity: '', // The server populates an `undefined` identity as ''. Use the same convention so we can do deep copy comparisons later.
server: registry.loginServer,
username: username,
passwordSecretRef: passwordName
Expand All @@ -45,6 +46,7 @@ export function getThirdPartyCredentialsAndSecrets(context: IContainerRegistryIm
const registries: RegistryCredentials[] = containerAppSettings?.registries?.filter(r => r.server !== loginServer) ?? [];
registries?.push(
{
identity: '', // The server populates an `undefined` identity as ''. Use the same convention so we can do deep copy comparisons later.
server: loginServer,
username: context.username,
passwordSecretRef
Expand Down
45 changes: 45 additions & 0 deletions src/commands/image/updateImage/UpdateImageDraftStep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import type { Revision } from "@azure/arm-appcontainers";
import { nonNullProp, randomUtils } from "@microsoft/vscode-azext-utils";
import type { Progress } from "vscode";
import { ext } from "../../../extensionVariables";
import type { ContainerAppItem, ContainerAppModel } from "../../../tree/ContainerAppItem";
import type { RevisionsItemModel } from "../../../tree/revisionManagement/RevisionItem";
import { localize } from "../../../utils/localize";
import { getParentResourceFromItem } from "../../../utils/revisionDraftUtils";
import { RevisionDraftUpdateBaseStep } from "../../revisionDraft/RevisionDraftUpdateBaseStep";
import { getContainerNameForImage } from "../imageSource/containerRegistry/getContainerNameForImage";
import type { UpdateImageContext } from "./updateImage";

export class UpdateImageDraftStep<T extends UpdateImageContext> extends RevisionDraftUpdateBaseStep<T> {
public priority: number = 490;

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

public async execute(context: UpdateImageContext, progress: Progress<{ message?: string | undefined; increment?: number | undefined }>): Promise<void> {
progress.report({ message: localize('updatingImage', 'Updating image (draft)...') });

this.revisionDraftTemplate.containers = [];
this.revisionDraftTemplate.containers.push({
env: context.environmentVariables,
image: context.image,
// We need the revision draft to always show up as having unsaved changes, we can ensure this by adding a unique ID at end of the container name
name: getContainerNameForImage(nonNullProp(context, 'image')) + `-${randomUtils.getRandomHexString(5)}`,
});

this.updateRevisionDraftWithTemplate();

const parentResource: ContainerAppModel | Revision = getParentResourceFromItem(this.baseItem);
ext.outputChannel.appendLog(localize('updatedImage', 'Updated container app "{0}" with image "{1}" (draft).', parentResource.name, context.image));
}

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

import type { RegistryCredentials, Secret } from "@azure/arm-appcontainers";
import { AzureWizardExecuteStep, nonNullProp } from "@microsoft/vscode-azext-utils";
import * as deepEqual from "deep-eql";
import type { Progress } from "vscode";
import { ext } from "../../../extensionVariables";
import { ContainerAppModel, getContainerEnvelopeWithSecrets } from "../../../tree/ContainerAppItem";
import { localize } from "../../../utils/localize";
import { updateContainerApp } from "../../updateContainerApp";
import type { UpdateImageContext } from "./updateImage";

export class UpdateRegistryAndSecretsStep extends AzureWizardExecuteStep<UpdateImageContext> {
public priority: number = 480;

public async execute(context: UpdateImageContext, progress: Progress<{ message?: string | undefined; increment?: number | undefined }>): Promise<void> {
const containerApp: ContainerAppModel = nonNullProp(context, 'containerApp');
const containerAppEnvelope = await getContainerEnvelopeWithSecrets(context, context.subscription, containerApp);

// If the credentials have not changed, we can skip this update
if (
this.areSecretsDeepEqual(containerAppEnvelope.configuration.secrets, context.secrets) &&
this.areRegistriesDeepEqual(containerAppEnvelope.configuration.registries, context.registries)
) {
return;
}

progress.report({ message: localize('configuringSecrets', 'Configuring registry secrets...') });

containerAppEnvelope.configuration.secrets = context.secrets;
containerAppEnvelope.configuration.registries = context.registries;

await updateContainerApp(context, context.subscription, containerAppEnvelope);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The whole update/save thing would apply to all these strings as well if we decide to make a change.

image


ext.outputChannel.appendLog(localize('updatedSecrets', 'Updated container app "{0}" with new registry secrets.', containerApp.name));
}

public shouldExecute(context: UpdateImageContext): boolean {
return !!context.registries && !!context.secrets;
}

private areSecretsDeepEqual(originalSecrets: Secret[] | undefined, newSecrets: Secret[] | undefined): boolean {
originalSecrets?.sort((a, b) => sortAlphabeticallyByKey(a, b, 'name'));
newSecrets?.sort((a, b) => sortAlphabeticallyByKey(a, b, 'name'));
return deepEqual(originalSecrets, newSecrets);
}

private areRegistriesDeepEqual(originalRegistries: RegistryCredentials[] | undefined, newRegistries: RegistryCredentials[] | undefined): boolean {
originalRegistries?.sort((a, b) => sortAlphabeticallyByKey(a, b, 'passwordSecretRef'));
newRegistries?.sort((a, b) => sortAlphabeticallyByKey(a, b, 'passwordSecretRef'));
return deepEqual(originalRegistries, newRegistries);
}
}

function sortAlphabeticallyByKey<T extends Secret | RegistryCredentials>(a: T, b: T, key: keyof T): number {
if (typeof a[key] !== 'string' || typeof b[key] !== 'string') {
return 0;
}

const valOne = a[key] as string;
const valTwo = b[key] as string;
return valOne.localeCompare(valTwo);
}
77 changes: 77 additions & 0 deletions src/commands/image/updateImage/updateImage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.md in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { KnownActiveRevisionsMode, Revision } from "@azure/arm-appcontainers";
import { VerifyProvidersStep } from "@microsoft/vscode-azext-azureutils";
import { AzureWizard, AzureWizardExecuteStep, AzureWizardPromptStep, ExecuteActivityContext, IActionContext, createSubscriptionContext } from "@microsoft/vscode-azext-utils";
import { webProvider } from "../../../constants";
import { ext } from "../../../extensionVariables";
import type { ContainerAppItem, ContainerAppModel } from "../../../tree/ContainerAppItem";
import type { RevisionDraftItem } from "../../../tree/revisionManagement/RevisionDraftItem";
import type { RevisionItem } from "../../../tree/revisionManagement/RevisionItem";
import { createActivityContext } from "../../../utils/activity/activityUtils";
import { localize } from "../../../utils/localize";
import { pickContainerApp } from "../../../utils/pickItem/pickContainerApp";
import { pickRevision, pickRevisionDraft } from "../../../utils/pickItem/pickRevision";
import { getParentResourceFromItem } from "../../../utils/revisionDraftUtils";
import type { ImageSourceBaseContext } from "../imageSource/ImageSourceBaseContext";
import { ImageSourceListStep } from "../imageSource/ImageSourceListStep";
import { UpdateImageDraftStep } from "./UpdateImageDraftStep";
import { UpdateRegistryAndSecretsStep } from "./UpdateRegistryAndSecretsStep";

export type UpdateImageContext = ImageSourceBaseContext & ExecuteActivityContext;

/**
* An ACA exclusive command that updates the container app or revision's container image via revision draft.
* The draft must be deployed for the changes to take effect and can be used to bundle together template changes.
*/
export async function updateImage(context: IActionContext, node?: ContainerAppItem | RevisionItem): Promise<void> {
let item: ContainerAppItem | RevisionItem | RevisionDraftItem | undefined = node;
if (!item) {
const containerAppItem: ContainerAppItem = await pickContainerApp(context);

if (containerAppItem.containerApp.revisionsMode === KnownActiveRevisionsMode.Single) {
item = containerAppItem;
} else {
if (ext.revisionDraftFileSystem.doesContainerAppsItemHaveRevisionDraft(containerAppItem)) {
item = await pickRevisionDraft(context, containerAppItem);
} else {
item = await pickRevision(context, containerAppItem);
}
}
}

const { subscription, containerApp } = item;

const wizardContext: UpdateImageContext = {
...context,
...createSubscriptionContext(subscription),
...await createActivityContext(),
subscription,
containerApp
};

const promptSteps: AzureWizardPromptStep<UpdateImageContext>[] = [
new ImageSourceListStep(),
];

const executeSteps: AzureWizardExecuteStep<UpdateImageContext>[] = [
new VerifyProvidersStep([webProvider]),
new UpdateRegistryAndSecretsStep(),
new UpdateImageDraftStep(item)
];

const parentResource: ContainerAppModel | Revision = getParentResourceFromItem(item);

const wizard: AzureWizard<UpdateImageContext> = new AzureWizard(wizardContext, {
title: localize('updateImage', 'Update container image for "{0}" (draft)', parentResource.name),
promptSteps,
executeSteps,
showLoadingPrompt: true
});

await wizard.prompt();
await wizard.execute();
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { IngressUpdateStepBase } from "../IngressUpdateStepBase";
import { isIngressEnabled } from "../isIngressEnabled";

export class DisableIngressStep extends IngressUpdateStepBase<IngressContext> {
public priority: number = 650;
public priority: number = 750;

public async execute(context: IngressContext, progress: Progress<{ message?: string | undefined; increment?: number | undefined }>): Promise<void> {
const containerApp = nonNullProp(context, 'containerApp');
Expand Down
2 changes: 1 addition & 1 deletion src/commands/ingress/enableIngress/EnableIngressStep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { updateContainerApp } from "../../updateContainerApp";
import type { IngressContext } from "../IngressContext";

export class EnableIngressStep extends ExecuteActivityOutputStepBase<IngressContext> {
public priority: number = 650;
public priority: number = 750;

protected async executeCore(context: IngressContext, progress: Progress<{ message?: string | undefined; increment?: number | undefined }>): Promise<void> {
progress.report({ message: localize('enablingIngress', 'Enabling ingress...') });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type { IngressContext } from "../IngressContext";
import { IngressUpdateStepBase } from "../IngressUpdateStepBase";

export class ToggleIngressVisibilityStep extends IngressUpdateStepBase<IngressContext> {
public priority: number = 650;
public priority: number = 750;

public async execute(context: IngressContext, progress: Progress<{ message?: string | undefined; increment?: number | undefined }>): Promise<void> {
const containerApp = nonNullProp(context, 'containerApp');
Expand Down
Loading