Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
- UpdateImageStep: 490 (revision draft)
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.

Maybe we should just call this CreateRevisionDraft internally?


### 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
5 changes: 5 additions & 0 deletions src/commands/image/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
## Commands

- `deployImageApi` - A command shared with the `vscode-docker` extension. It uses our old `deployImage` command flow which immediately tries to deploy the image to a container app without creating a draft. This command cannot be used to bundle template changes.

- `updateImage` - 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.
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.

Had a similar comment in your other PR. #476 (comment)

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: '',
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.

What's the dealio with this?

Copy link
Copy Markdown
Contributor Author

@MicroFish91 MicroFish91 Oct 11, 2023

Choose a reason for hiding this comment

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

The server populates an undefined identity as '', so when I deep copy compare in the later steps in UpdateRegistryAndSecretsStep, I need to add this here to keep them being detected as equivalent. I forgot but I should probably add a comment explaining this

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: '',
server: loginServer,
username: context.username,
passwordSecretRef
Expand Down
45 changes: 45 additions & 0 deletions src/commands/image/updateImage/UpdateImageStep.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 UpdateImageStep<T extends UpdateImageContext> extends RevisionDraftUpdateBaseStep<T> {
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 mentioned it in the execute priority markdown, but I think it makes more sense to call this CreateRevisionDraft or CreateImageDraft. Update feels like the wrong word here since it can mean remotely or locally. Update and upload also look super similar, so I feel like the brain just fills in the gaps.

Maybe we should use the word save? Save tends to mean locally, or at least in place?

Copy link
Copy Markdown
Contributor Author

@MicroFish91 MicroFish91 Oct 11, 2023

Choose a reason for hiding this comment

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

Yeah let me look through some of the other steps and I'll get back to you. Maybe we can brainstorm a naming convention that works, I was thinking something similar before but didn't have any ideas that stuck.

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);
}
73 changes: 73 additions & 0 deletions src/commands/image/updateImage/updateImage.ts
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.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 { UpdateImageStep } from "./UpdateImageStep";
import { UpdateRegistryAndSecretsStep } from "./UpdateRegistryAndSecretsStep";

export type UpdateImageContext = ImageSourceBaseContext & ExecuteActivityContext;

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 UpdateImageStep(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