Skip to content

Commit ca50bc6

Browse files
authored
Enable updating revision drafts through commands & leverage with Scale Rules (#423)
1 parent 35eaafd commit ca50bc6

26 files changed

+575
-284
lines changed

src/commands/revision/chooseRevisionMode/chooseRevisionMode.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export async function chooseRevisionMode(context: IActionContext, item?: Contain
1919
item ??= await pickContainerApp(context);
2020

2121
let hasRevisionDraft: boolean | undefined;
22-
if (item instanceof ContainerAppItem) {
22+
if (ContainerAppItem.isContainerAppItem(item)) {
2323
// A revision draft can exist but may be identical to the source, distinguishing the difference in single revisions mode
2424
// improves the user experience by allowing us to skip the confirm step, silently discarding drafts instead
2525
hasRevisionDraft = item.hasUnsavedChanges();
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
## Detecting Unsaved Changes
2+
3+
When comparing items for changes in single revision mode, prefer referencing the `containerApp` template. When comparing items in multiple revisions mode, prefer referencing the `revision` template.
4+
5+
Reason: Even though the `containerApp` template is essentially equivalent to the `latest` revision template... sometimes there are micro-differences present. Although they end up being functionally equivalent, they may not always be equivalent enough to consistently pass a deep copy test (which is how we are set up to detect unsaved changes).
6+
7+
## Data Sources for Tree Items
8+
9+
Until the addition of revision drafts, the view has always reflected only one source of truth - the latest deployed changes. With the addition of revision drafts, the view now prioritizes showing the latest draft `Unsaved changes` when they are present. Model properties `containerApp` and `revision` should be kept consistent with the latest deployed changes so that methods like `hasUnsavedChanges` always have a reliable data reference for deep copy comparison.

src/commands/revisionDraft/RevisionDraftFileSystem.ts

Lines changed: 21 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,14 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import type { Template } from "@azure/arm-appcontainers";
6+
import { KnownActiveRevisionsMode, type Template } from "@azure/arm-appcontainers";
77
import { nonNullValueAndProp } from "@microsoft/vscode-azext-utils";
88
import { Disposable, Event, EventEmitter, FileChangeEvent, FileChangeType, FileStat, FileSystemProvider, FileType, TextDocument, Uri, window, workspace } from "vscode";
99
import { URI } from "vscode-uri";
1010
import { ext } from "../../extensionVariables";
1111
import { ContainerAppItem } from "../../tree/ContainerAppItem";
1212
import type { ContainerAppsItem } from "../../tree/ContainerAppsBranchDataProvider";
13-
import type { RevisionDraftItem } from "../../tree/revisionManagement/RevisionDraftItem";
14-
import type { RevisionItem } from "../../tree/revisionManagement/RevisionItem";
13+
import type { RevisionsItemModel } from "../../tree/revisionManagement/RevisionItem";
1514
import { localize } from "../../utils/localize";
1615

1716
const notSupported: string = localize('notSupported', 'This operation is not currently supported.');
@@ -50,23 +49,15 @@ export class RevisionDraftFileSystem implements FileSystemProvider {
5049
return this.emitter.event;
5150
}
5251

53-
// Create
54-
createRevisionDraft(item: ContainerAppItem | RevisionItem | RevisionDraftItem): void {
52+
createRevisionDraft(item: ContainerAppItem | RevisionsItemModel): void {
5553
const uri: Uri = this.buildUriFromItem(item);
5654
if (this.draftStore.has(uri.path)) {
5755
return;
5856
}
5957

58+
// Branching path reasoning: https://github.com/microsoft/vscode-azurecontainerapps/blob/main/src/commands/revisionDraft/README.md
6059
let file: RevisionDraftFile | undefined;
61-
if (item instanceof ContainerAppItem) {
62-
/**
63-
* Sometimes there are micro-differences present between the latest revision template and the container app template.
64-
* They end up being essentially equivalent, but not so equivalent as to always pass a deep copy test (which is how we detect unsaved changes).
65-
*
66-
* Since only container app template data is shown in single revision mode, and since revision data is not directly present on
67-
* the container app tree item without further changes, it is easier to just use the container app template
68-
* as the primary source of truth when in single revision mode.
69-
*/
60+
if (ContainerAppItem.isContainerAppItem(item) || item.containerApp.revisionsMode === KnownActiveRevisionsMode.Single) {
7061
const revisionContent: Uint8Array = Buffer.from(JSON.stringify(nonNullValueAndProp(item.containerApp, 'template'), undefined, 4));
7162
file = new RevisionDraftFile(revisionContent, item.containerApp.id, nonNullValueAndProp(item.containerApp, 'latestRevisionName'));
7263
} else {
@@ -78,8 +69,7 @@ export class RevisionDraftFileSystem implements FileSystemProvider {
7869
this.fireSoon({ type: FileChangeType.Created, uri });
7970
}
8071

81-
// Read
82-
parseRevisionDraft<T extends ContainerAppsItem>(item: T): Template | undefined {
72+
parseRevisionDraft(item: ContainerAppsItem): Template | undefined {
8373
const uri: URI = this.buildUriFromItem(item);
8474
if (!this.draftStore.has(uri.path)) {
8575
return undefined;
@@ -93,12 +83,12 @@ export class RevisionDraftFileSystem implements FileSystemProvider {
9383
return contents ? Buffer.from(contents) : Buffer.from('');
9484
}
9585

96-
doesContainerAppsItemHaveRevisionDraft<T extends ContainerAppsItem>(item: T): boolean {
86+
doesContainerAppsItemHaveRevisionDraft(item: ContainerAppsItem): boolean {
9787
const uri: Uri = this.buildUriFromItem(item);
9888
return this.draftStore.has(uri.path);
9989
}
10090

101-
getRevisionDraftFile<T extends ContainerAppsItem>(item: T): RevisionDraftFile | undefined {
91+
getRevisionDraftFile(item: ContainerAppsItem): RevisionDraftFile | undefined {
10292
const uri: Uri = this.buildUriFromItem(item);
10393
return this.draftStore.get(uri.path);
10494
}
@@ -118,8 +108,7 @@ export class RevisionDraftFileSystem implements FileSystemProvider {
118108
}
119109
}
120110

121-
// Update
122-
async editRevisionDraft(item: ContainerAppItem | RevisionItem | RevisionDraftItem): Promise<void> {
111+
async editRevisionDraft(item: ContainerAppItem | RevisionsItemModel): Promise<void> {
123112
const uri: Uri = this.buildUriFromItem(item);
124113
if (!this.draftStore.has(uri.path)) {
125114
this.createRevisionDraft(item);
@@ -146,8 +135,17 @@ export class RevisionDraftFileSystem implements FileSystemProvider {
146135
ext.state.notifyChildrenChanged(file.containerAppId);
147136
}
148137

149-
// Delete
150-
discardRevisionDraft<T extends ContainerAppsItem>(item: T): void {
138+
updateRevisionDraftWithTemplate(item: RevisionsItemModel, template: Template): void {
139+
const uri: Uri = this.buildUriFromItem(item);
140+
if (!this.draftStore.has(uri.path)) {
141+
this.createRevisionDraft(item);
142+
}
143+
144+
const newContent: Uint8Array = Buffer.from(JSON.stringify(template, undefined, 4));
145+
this.writeFile(uri, newContent);
146+
}
147+
148+
discardRevisionDraft(item: ContainerAppsItem): void {
151149
const uri: Uri = this.buildUriFromItem(item);
152150
if (!this.draftStore.has(uri.path)) {
153151
return;
@@ -161,8 +159,7 @@ export class RevisionDraftFileSystem implements FileSystemProvider {
161159
this.fireSoon({ type: FileChangeType.Deleted, uri });
162160
}
163161

164-
// Helper
165-
private buildUriFromItem<T extends ContainerAppsItem>(item: T): Uri {
162+
private buildUriFromItem(item: ContainerAppsItem): Uri {
166163
return URI.parse(`${RevisionDraftFileSystem.scheme}:/${item.containerApp.name}.json`);
167164
}
168165

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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 { Template } from "@azure/arm-appcontainers";
7+
import { AzureWizardExecuteStep, nonNullValueAndProp } from "@microsoft/vscode-azext-utils";
8+
import type { Progress } from "vscode";
9+
import { ext } from "../../extensionVariables";
10+
import type { RevisionsItemModel } from "../../tree/revisionManagement/RevisionItem";
11+
import type { IContainerAppContext } from "../IContainerAppContext";
12+
13+
export abstract class RevisionDraftUpdateBaseStep<T extends IContainerAppContext> extends AzureWizardExecuteStep<T> {
14+
/**
15+
* This property holds the active template revisions used for updating the revision draft
16+
*/
17+
protected revisionDraftTemplate: Template;
18+
19+
constructor(readonly baseItem: RevisionsItemModel) {
20+
super();
21+
this.revisionDraftTemplate = this.initRevisionDraftTemplate();
22+
}
23+
24+
abstract execute(context: T, progress: Progress<{ message?: string | undefined; increment?: number | undefined }>): Promise<void>;
25+
abstract shouldExecute(context: T): boolean;
26+
27+
/**
28+
* Call this method to upload `revisionDraftTemplate` changes to the container app revision draft
29+
*/
30+
protected updateRevisionDraftWithTemplate(): void {
31+
ext.revisionDraftFileSystem.updateRevisionDraftWithTemplate(this.baseItem, this.revisionDraftTemplate);
32+
}
33+
34+
private initRevisionDraftTemplate(): Template {
35+
let template: Template | undefined = ext.revisionDraftFileSystem.parseRevisionDraft(this.baseItem);
36+
if (!template) {
37+
template = nonNullValueAndProp(this.baseItem.revision, 'template');
38+
}
39+
return template;
40+
}
41+
}

src/commands/scaling/addScaleRule/AddScaleRuleStep.ts

Lines changed: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,55 +3,54 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import type { ScaleRule, Template } from "@azure/arm-appcontainers";
7-
import { AzureWizardExecuteStep, nonNullProp } from "@microsoft/vscode-azext-utils";
8-
import { Progress, window } from "vscode";
6+
import { type ScaleRule } from "@azure/arm-appcontainers";
7+
import { nonNullProp } from "@microsoft/vscode-azext-utils";
98
import { ScaleRuleTypes } from "../../../constants";
109
import { ext } from "../../../extensionVariables";
10+
import type { RevisionsItemModel } from "../../../tree/revisionManagement/RevisionItem";
1111
import { localize } from "../../../utils/localize";
12-
import { updateContainerApp } from "../../../utils/updateContainerApp";
12+
import { getParentResource } from "../../../utils/revisionDraftUtils";
13+
import { RevisionDraftUpdateBaseStep } from "../../revisionDraft/RevisionDraftUpdateBaseStep";
1314
import type { IAddScaleRuleContext } from "./IAddScaleRuleContext";
1415

15-
export class AddScaleRuleStep extends AzureWizardExecuteStep<IAddScaleRuleContext> {
16-
public priority: number = 100;
16+
export class AddScaleRuleStep<T extends IAddScaleRuleContext> extends RevisionDraftUpdateBaseStep<T> {
17+
public priority: number = 200;
1718

18-
public async execute(context: IAddScaleRuleContext, _progress: Progress<{ message?: string | undefined; increment?: number | undefined }>): Promise<void> {
19-
const adding = localize('addingScaleRule', 'Adding {0} rule "{1}" to "{2}"...', context.ruleType, context.ruleName, context.containerApp.name);
20-
const added = localize('addedScaleRule', 'Successfully added {0} rule "{1}" to "{2}".', context.ruleType, context.ruleName, context.containerApp.name);
19+
constructor(baseItem: RevisionsItemModel) {
20+
super(baseItem);
21+
}
2122

22-
const template: Template = context.containerApp.template || {};
23-
template.scale = context.scale || {};
24-
template.scale.rules = context.scaleRules || [];
23+
public async execute(context: IAddScaleRuleContext): Promise<void> {
24+
this.revisionDraftTemplate.scale ||= {};
25+
this.revisionDraftTemplate.scale.rules ||= [];
2526

26-
const scaleRule: ScaleRule = this.buildRule(context);
27-
this.integrateRule(context, template.scale.rules, scaleRule);
27+
context.scaleRule = this.buildRule(context);
28+
this.integrateRule(context, this.revisionDraftTemplate.scale.rules, context.scaleRule);
29+
this.updateRevisionDraftWithTemplate();
2830

29-
ext.outputChannel.appendLog(adding);
30-
await updateContainerApp(context, context.subscription, context.containerApp, { template });
31-
context.scaleRule = scaleRule;
32-
void window.showInformationMessage(added);
33-
ext.outputChannel.appendLog(added);
31+
const resourceName = getParentResource(context.containerApp, this.baseItem.revision).name;
32+
ext.outputChannel.appendLog(localize('addedScaleRule', 'Added {0} rule "{1}" to "{2}" (draft)', context.newRuleType, context.newRuleName, resourceName));
3433
}
3534

3635
public shouldExecute(context: IAddScaleRuleContext): boolean {
37-
return context.ruleName !== undefined && context.ruleType !== undefined;
36+
return !!context.newRuleName && !!context.newRuleType;
3837
}
3938

4039
private buildRule(context: IAddScaleRuleContext): ScaleRule {
41-
const scaleRule: ScaleRule = { name: context.ruleName };
42-
switch (context.ruleType) {
40+
const scaleRule: ScaleRule = { name: context.newRuleName };
41+
switch (context.newRuleType) {
4342
case ScaleRuleTypes.HTTP:
4443
scaleRule.http = {
4544
metadata: {
46-
concurrentRequests: nonNullProp(context, 'concurrentRequests')
45+
concurrentRequests: nonNullProp(context, 'newHttpConcurrentRequests')
4746
}
4847
};
4948
break;
5049
case ScaleRuleTypes.Queue:
5150
scaleRule.azureQueue = {
52-
queueName: context.queueName,
53-
queueLength: context.queueLength,
54-
auth: [{ secretRef: context.secretRef, triggerParameter: context.triggerParameter }]
51+
queueName: context.newQueueName,
52+
queueLength: context.newQueueLength,
53+
auth: [{ secretRef: context.newQueueSecretRef, triggerParameter: context.newQueueTriggerParameter }]
5554
}
5655
break;
5756
default:
@@ -60,12 +59,12 @@ export class AddScaleRuleStep extends AzureWizardExecuteStep<IAddScaleRuleContex
6059
}
6160

6261
private integrateRule(context: IAddScaleRuleContext, scaleRules: ScaleRule[], scaleRule: ScaleRule): void {
63-
switch (context.ruleType) {
62+
switch (context.newRuleType) {
6463
case ScaleRuleTypes.HTTP:
6564
// Portal only allows one HTTP rule per revision
6665
const idx: number = scaleRules.findIndex((rule) => rule.http);
6766
if (idx !== -1) {
68-
scaleRules.splice(idx, 0);
67+
scaleRules.splice(idx, 1);
6968
}
7069
break;
7170
case ScaleRuleTypes.Queue:

src/commands/scaling/addScaleRule/IAddScaleRuleContext.ts

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,32 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import type { Scale, ScaleRule } from "@azure/arm-appcontainers";
6+
import { ScaleRule } from "@azure/arm-appcontainers";
7+
import type { ExecuteActivityContext } from "@microsoft/vscode-azext-utils";
78
import type { ContainerAppModel } from "../../../tree/ContainerAppItem";
89
import type { IContainerAppContext } from "../../IContainerAppContext";
910

10-
export interface IAddScaleRuleContext extends IContainerAppContext {
11+
export interface IAddScaleRuleContext extends IContainerAppContext, ExecuteActivityContext {
1112
// Make containerApp _required_
1213
containerApp: ContainerAppModel;
1314

14-
scale: Scale;
15-
scaleRules: ScaleRule[];
15+
/**
16+
* The name of the parent resource (`ContainerAppModel | Revision`)
17+
*/
18+
parentResourceName: string;
1619

1720
// Base Rule Properties
18-
ruleName?: string;
19-
ruleType?: string;
21+
newRuleName?: string;
22+
newRuleType?: string;
2023

2124
// HTTP Rule Properties
22-
concurrentRequests?: string;
25+
newHttpConcurrentRequests?: string;
2326

2427
// Queue Rule Properties
25-
queueName?: string;
26-
queueLength?: number;
27-
secretRef?: string;
28-
triggerParameter?: string;
28+
newQueueName?: string;
29+
newQueueLength?: number;
30+
newQueueSecretRef?: string;
31+
newQueueTriggerParameter?: string;
2932

3033
scaleRule?: ScaleRule;
3134
}

0 commit comments

Comments
 (0)