Skip to content

Commit 657473c

Browse files
authored
Add command updateImage for updating a container image via revision draft (#477)
1 parent 1c85377 commit 657473c

File tree

19 files changed

+263
-42
lines changed

19 files changed

+263
-42
lines changed

package.json

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@
7575
"title": "%containerApps.editContainerApp%",
7676
"category": "Azure Container Apps"
7777
},
78+
{
79+
"command": "containerApps.updateImage",
80+
"title": "%containerApps.updateImage%",
81+
"category": "Azure Container Apps"
82+
},
7883
{
7984
"command": "containerApps.deployImageApi",
8085
"title": "%containerApps.deployImageApi%",
@@ -328,6 +333,11 @@
328333
"when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /containerAppItem/i",
329334
"group": "5@1"
330335
},
336+
{
337+
"command": "containerApps.updateImage",
338+
"when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /containerAppItem(.*)revisionMode:single/i",
339+
"group": "6@1"
340+
},
331341
{
332342
"command": "containerApps.startStreamingLogs",
333343
"when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /containerAppItem/i",
@@ -368,6 +378,11 @@
368378
"when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /revisionItem(.*)revisionState:active/i",
369379
"group": "2@4"
370380
},
381+
{
382+
"command": "containerApps.updateImage",
383+
"when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /revisionDraft:false(.*)revisionItem/i",
384+
"group": "3@1"
385+
},
371386
{
372387
"command": "containerApps.deployRevisionDraft",
373388
"when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /revisionDraftItem(.*)unsavedChanges:true/i",
@@ -389,10 +404,15 @@
389404
"group": "1@2"
390405
},
391406
{
392-
"command": "containerApps.editRevisionDraft",
407+
"command": "containerApps.updateImage",
393408
"when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /revisionDraftItem/i",
394409
"group": "2@1"
395410
},
411+
{
412+
"command": "containerApps.editRevisionDraft",
413+
"when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /revisionDraftItem/i",
414+
"group": "3@1"
415+
},
396416
{
397417
"command": "containerApps.editScaleRange",
398418
"when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /scaleItem/i",

package.nls.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"containerApps.browse": "Browse",
88
"containerApps.createContainerApp": "Create Container App...",
99
"containerApps.editContainerApp": "Edit Container App (Advanced)...",
10+
"containerApps.updateImage": "Update Container Image...",
1011
"containerApps.deployImageApi": "Deploy Image to Container App (API)...",
1112
"containerApps.deployWorkspaceProject": "Deploy Project from Workspace...",
1213
"containerApps.deleteContainerApp": "Delete Container App...",

src/commands/EXECUTE_PRIORITY.md

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,53 +22,56 @@ When creating or updating resources, execute steps should occupy certain priorit
2222

2323
- RegistryCreateStep: 350
2424

25-
### 3. Image Source
25+
### 3. Image
2626

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

29-
#### Build Image in Azure Steps
29+
#### General Steps
30+
##### Build Image in Azure Steps
3031

3132
- TarFileStep: 420
3233
- UploadSourceCodeStep: 430
3334
- RunStep: 440
3435
- BuildImageStep: 450
3536
- ContainerRegistryImageConfigureStep: 470
3637

37-
#### Container Registry Steps
38+
##### Container Registry Steps
3839

3940
- ContainerRegistryImageConfigureStep: 470
4041

41-
#### Common Steps
42+
#### `updateImage` Steps
4243

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

45-
### 4. Environment Variables
47+
### 4. Unallocated Space
4648

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

4951
#### Steps
5052

5153
Reserved
5254

53-
### 5. Ingress
55+
### 5. Container App
5456

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

5759
#### Steps
5860

59-
- EnableIngressStep: 650
60-
- DisableIngressStep: 650
61+
- ContainerAppCreateStep: 620
62+
- ContainerAppUpdateStep: 650
6163

62-
- TargetPortUpdateStep: 650 (single command only)
63-
- ToggleIngressVisibilityStep: 650 (single command only)
64-
65-
### 6. Container App
64+
### 6. Ingress
6665

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

6968
#### Steps
7069

71-
- ContainerAppCreateStep: 750
70+
- EnableIngressStep: 750 (update existing container app)
71+
- DisableIngressStep: 750 (update existing container app)
72+
73+
- TargetPortUpdateStep: 750 (single command only)
74+
- ToggleIngressVisibilityStep: 750 (single command only)
7275

7376
### 7. Secrets
7477

@@ -93,8 +96,7 @@ Reserved
9396

9497
#### Steps
9598

96-
- ScaleRangeUpdateStep: 1110
97-
- AddScaleRuleStep: 1120
99+
- AddScaleRuleStep: 1120 (revision draft)
98100

99101
### 10. Unallocated Space
100102

src/commands/createContainerApp/ContainerAppCreateStep.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { getContainerNameForImage } from "../image/imageSource/containerRegistry
1717
import type { ICreateContainerAppContext } from "./ICreateContainerAppContext";
1818

1919
export class ContainerAppCreateStep extends ExecuteActivityOutputStepBase<ICreateContainerAppContext> {
20-
public priority: number = 750;
20+
public priority: number = 620;
2121

2222
protected async executeCore(context: ICreateContainerAppContext, progress: Progress<{ message?: string | undefined; increment?: number | undefined }>): Promise<void> {
2323
const appClient: ContainerAppsAPIClient = await createContainerAppsAPIClient(context);

src/commands/image/imageSource/containerRegistry/getRegistryCredentialsAndSecrets.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export async function getAcrCredentialsAndSecrets(context: IContainerRegistryIma
2323
const registries: RegistryCredentials[] = containerAppSettings?.registries?.filter(r => r.server !== registry.loginServer) ?? [];
2424
registries?.push(
2525
{
26+
identity: '', // The server populates an `undefined` identity as ''. Use the same convention so we can do deep copy comparisons later.
2627
server: registry.loginServer,
2728
username: username,
2829
passwordSecretRef: passwordName
@@ -45,6 +46,7 @@ export function getThirdPartyCredentialsAndSecrets(context: IContainerRegistryIm
4546
const registries: RegistryCredentials[] = containerAppSettings?.registries?.filter(r => r.server !== loginServer) ?? [];
4647
registries?.push(
4748
{
49+
identity: '', // The server populates an `undefined` identity as ''. Use the same convention so we can do deep copy comparisons later.
4850
server: loginServer,
4951
username: context.username,
5052
passwordSecretRef
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import type { Revision } from "@azure/arm-appcontainers";
7+
import { nonNullProp, randomUtils } from "@microsoft/vscode-azext-utils";
8+
import type { Progress } from "vscode";
9+
import { ext } from "../../../extensionVariables";
10+
import type { ContainerAppItem, ContainerAppModel } from "../../../tree/ContainerAppItem";
11+
import type { RevisionsItemModel } from "../../../tree/revisionManagement/RevisionItem";
12+
import { localize } from "../../../utils/localize";
13+
import { getParentResourceFromItem } from "../../../utils/revisionDraftUtils";
14+
import { RevisionDraftUpdateBaseStep } from "../../revisionDraft/RevisionDraftUpdateBaseStep";
15+
import { getContainerNameForImage } from "../imageSource/containerRegistry/getContainerNameForImage";
16+
import type { UpdateImageContext } from "./updateImage";
17+
18+
export class UpdateImageDraftStep<T extends UpdateImageContext> extends RevisionDraftUpdateBaseStep<T> {
19+
public priority: number = 490;
20+
21+
constructor(baseItem: ContainerAppItem | RevisionsItemModel) {
22+
super(baseItem);
23+
}
24+
25+
public async execute(context: UpdateImageContext, progress: Progress<{ message?: string | undefined; increment?: number | undefined }>): Promise<void> {
26+
progress.report({ message: localize('updatingImage', 'Updating image (draft)...') });
27+
28+
this.revisionDraftTemplate.containers = [];
29+
this.revisionDraftTemplate.containers.push({
30+
env: context.environmentVariables,
31+
image: context.image,
32+
// 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
33+
name: getContainerNameForImage(nonNullProp(context, 'image')) + `-${randomUtils.getRandomHexString(5)}`,
34+
});
35+
36+
this.updateRevisionDraftWithTemplate();
37+
38+
const parentResource: ContainerAppModel | Revision = getParentResourceFromItem(this.baseItem);
39+
ext.outputChannel.appendLog(localize('updatedImage', 'Updated container app "{0}" with image "{1}" (draft).', parentResource.name, context.image));
40+
}
41+
42+
public shouldExecute(context: UpdateImageContext): boolean {
43+
return !!context.containerApp && !!context.image;
44+
}
45+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import type { RegistryCredentials, Secret } from "@azure/arm-appcontainers";
7+
import { AzureWizardExecuteStep, nonNullProp } from "@microsoft/vscode-azext-utils";
8+
import * as deepEqual from "deep-eql";
9+
import type { Progress } from "vscode";
10+
import { ext } from "../../../extensionVariables";
11+
import { ContainerAppModel, getContainerEnvelopeWithSecrets } from "../../../tree/ContainerAppItem";
12+
import { localize } from "../../../utils/localize";
13+
import { updateContainerApp } from "../../updateContainerApp";
14+
import type { UpdateImageContext } from "./updateImage";
15+
16+
export class UpdateRegistryAndSecretsStep extends AzureWizardExecuteStep<UpdateImageContext> {
17+
public priority: number = 480;
18+
19+
public async execute(context: UpdateImageContext, progress: Progress<{ message?: string | undefined; increment?: number | undefined }>): Promise<void> {
20+
const containerApp: ContainerAppModel = nonNullProp(context, 'containerApp');
21+
const containerAppEnvelope = await getContainerEnvelopeWithSecrets(context, context.subscription, containerApp);
22+
23+
// If the credentials have not changed, we can skip this update
24+
if (
25+
this.areSecretsDeepEqual(containerAppEnvelope.configuration.secrets, context.secrets) &&
26+
this.areRegistriesDeepEqual(containerAppEnvelope.configuration.registries, context.registries)
27+
) {
28+
return;
29+
}
30+
31+
progress.report({ message: localize('configuringSecrets', 'Configuring registry secrets...') });
32+
33+
containerAppEnvelope.configuration.secrets = context.secrets;
34+
containerAppEnvelope.configuration.registries = context.registries;
35+
36+
await updateContainerApp(context, context.subscription, containerAppEnvelope);
37+
38+
ext.outputChannel.appendLog(localize('updatedSecrets', 'Updated container app "{0}" with new registry secrets.', containerApp.name));
39+
}
40+
41+
public shouldExecute(context: UpdateImageContext): boolean {
42+
return !!context.registries && !!context.secrets;
43+
}
44+
45+
private areSecretsDeepEqual(originalSecrets: Secret[] | undefined, newSecrets: Secret[] | undefined): boolean {
46+
originalSecrets?.sort((a, b) => sortAlphabeticallyByKey(a, b, 'name'));
47+
newSecrets?.sort((a, b) => sortAlphabeticallyByKey(a, b, 'name'));
48+
return deepEqual(originalSecrets, newSecrets);
49+
}
50+
51+
private areRegistriesDeepEqual(originalRegistries: RegistryCredentials[] | undefined, newRegistries: RegistryCredentials[] | undefined): boolean {
52+
originalRegistries?.sort((a, b) => sortAlphabeticallyByKey(a, b, 'passwordSecretRef'));
53+
newRegistries?.sort((a, b) => sortAlphabeticallyByKey(a, b, 'passwordSecretRef'));
54+
return deepEqual(originalRegistries, newRegistries);
55+
}
56+
}
57+
58+
function sortAlphabeticallyByKey<T extends Secret | RegistryCredentials>(a: T, b: T, key: keyof T): number {
59+
if (typeof a[key] !== 'string' || typeof b[key] !== 'string') {
60+
return 0;
61+
}
62+
63+
const valOne = a[key] as string;
64+
const valTwo = b[key] as string;
65+
return valOne.localeCompare(valTwo);
66+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.md in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { KnownActiveRevisionsMode, Revision } from "@azure/arm-appcontainers";
7+
import { VerifyProvidersStep } from "@microsoft/vscode-azext-azureutils";
8+
import { AzureWizard, AzureWizardExecuteStep, AzureWizardPromptStep, ExecuteActivityContext, IActionContext, createSubscriptionContext } from "@microsoft/vscode-azext-utils";
9+
import { webProvider } from "../../../constants";
10+
import { ext } from "../../../extensionVariables";
11+
import type { ContainerAppItem, ContainerAppModel } from "../../../tree/ContainerAppItem";
12+
import type { RevisionDraftItem } from "../../../tree/revisionManagement/RevisionDraftItem";
13+
import type { RevisionItem } from "../../../tree/revisionManagement/RevisionItem";
14+
import { createActivityContext } from "../../../utils/activity/activityUtils";
15+
import { localize } from "../../../utils/localize";
16+
import { pickContainerApp } from "../../../utils/pickItem/pickContainerApp";
17+
import { pickRevision, pickRevisionDraft } from "../../../utils/pickItem/pickRevision";
18+
import { getParentResourceFromItem } from "../../../utils/revisionDraftUtils";
19+
import type { ImageSourceBaseContext } from "../imageSource/ImageSourceBaseContext";
20+
import { ImageSourceListStep } from "../imageSource/ImageSourceListStep";
21+
import { UpdateImageDraftStep } from "./UpdateImageDraftStep";
22+
import { UpdateRegistryAndSecretsStep } from "./UpdateRegistryAndSecretsStep";
23+
24+
export type UpdateImageContext = ImageSourceBaseContext & ExecuteActivityContext;
25+
26+
/**
27+
* An ACA exclusive command that updates the container app or revision's container image via revision draft.
28+
* The draft must be deployed for the changes to take effect and can be used to bundle together template changes.
29+
*/
30+
export async function updateImage(context: IActionContext, node?: ContainerAppItem | RevisionItem): Promise<void> {
31+
let item: ContainerAppItem | RevisionItem | RevisionDraftItem | undefined = node;
32+
if (!item) {
33+
const containerAppItem: ContainerAppItem = await pickContainerApp(context);
34+
35+
if (containerAppItem.containerApp.revisionsMode === KnownActiveRevisionsMode.Single) {
36+
item = containerAppItem;
37+
} else {
38+
if (ext.revisionDraftFileSystem.doesContainerAppsItemHaveRevisionDraft(containerAppItem)) {
39+
item = await pickRevisionDraft(context, containerAppItem);
40+
} else {
41+
item = await pickRevision(context, containerAppItem);
42+
}
43+
}
44+
}
45+
46+
const { subscription, containerApp } = item;
47+
48+
const wizardContext: UpdateImageContext = {
49+
...context,
50+
...createSubscriptionContext(subscription),
51+
...await createActivityContext(),
52+
subscription,
53+
containerApp
54+
};
55+
56+
const promptSteps: AzureWizardPromptStep<UpdateImageContext>[] = [
57+
new ImageSourceListStep(),
58+
];
59+
60+
const executeSteps: AzureWizardExecuteStep<UpdateImageContext>[] = [
61+
new VerifyProvidersStep([webProvider]),
62+
new UpdateRegistryAndSecretsStep(),
63+
new UpdateImageDraftStep(item)
64+
];
65+
66+
const parentResource: ContainerAppModel | Revision = getParentResourceFromItem(item);
67+
68+
const wizard: AzureWizard<UpdateImageContext> = new AzureWizard(wizardContext, {
69+
title: localize('updateImage', 'Update container image for "{0}" (draft)', parentResource.name),
70+
promptSteps,
71+
executeSteps,
72+
showLoadingPrompt: true
73+
});
74+
75+
await wizard.prompt();
76+
await wizard.execute();
77+
}

src/commands/ingress/disableIngress/DisableIngressStep.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { IngressUpdateStepBase } from "../IngressUpdateStepBase";
1111
import { isIngressEnabled } from "../isIngressEnabled";
1212

1313
export class DisableIngressStep extends IngressUpdateStepBase<IngressContext> {
14-
public priority: number = 650;
14+
public priority: number = 750;
1515

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

src/commands/ingress/enableIngress/EnableIngressStep.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { updateContainerApp } from "../../updateContainerApp";
1414
import type { IngressContext } from "../IngressContext";
1515

1616
export class EnableIngressStep extends ExecuteActivityOutputStepBase<IngressContext> {
17-
public priority: number = 650;
17+
public priority: number = 750;
1818

1919
protected async executeCore(context: IngressContext, progress: Progress<{ message?: string | undefined; increment?: number | undefined }>): Promise<void> {
2020
progress.report({ message: localize('enablingIngress', 'Enabling ingress...') });

0 commit comments

Comments
 (0)