Skip to content
Merged
1 change: 1 addition & 0 deletions extension.bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export * from './src/commands/ingress/tryGetDockerfileExposePorts';
export { activate, deactivate } from './src/extension';
export * from './src/extensionVariables';
export * from './src/utils/azureClients';
export * from './src/utils/imageNameUtils';
export * from './src/utils/settingUtils';
export * from './src/utils/validateUtils';

Expand Down
19 changes: 12 additions & 7 deletions src/commands/image/deployImageApi/deployImage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@
* Licensed under the MIT License. See License.md in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { AzureWizard, createSubscriptionContext, type AzureWizardExecuteStep, type AzureWizardPromptStep, type IActionContext, type ISubscriptionContext } from "@microsoft/vscode-azext-utils";
import { AzureWizard, createSubscriptionContext, type AzureWizardPromptStep, type IActionContext, type ISubscriptionContext } from "@microsoft/vscode-azext-utils";
import { type ContainerAppItem } from "../../../tree/ContainerAppItem";
import { createActivityContext } from "../../../utils/activityUtils";
import { getManagedEnvironmentFromContainerApp } from "../../../utils/getResourceUtils";
import { getVerifyProvidersStep } from "../../../utils/getVerifyProvidersStep";
import { getImageNameWithoutTag } from "../../../utils/imageNameUtils";
import { localize } from "../../../utils/localize";
import { ContainerAppOverwriteConfirmStep } from "../../ContainerAppOverwriteConfirmStep";
import { showContainerAppNotification } from "../../createContainerApp/showContainerAppNotification";
import { IngressPromptStep } from "../../ingress/IngressPromptStep";
import { ContainerAppUpdateStep } from "../imageSource/ContainerAppUpdateStep";
import { ImageSourceListStep } from "../imageSource/ImageSourceListStep";
import { type ContainerRegistryImageSourceContext } from "../imageSource/containerRegistry/ContainerRegistryImageSourceContext";
Expand All @@ -33,18 +35,21 @@ export async function deployImage(context: IActionContext & Partial<ContainerReg

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

const executeSteps: AzureWizardExecuteStep<DeployImageApiContext>[] = [
getVerifyProvidersStep<DeployImageApiContext>(),
new ContainerAppUpdateStep()
];
// If more than the image tag changed, prompt for ingress again
if (getImageNameWithoutTag(wizardContext.containerApp?.template?.containers?.[0].image ?? '') !== getImageNameWithoutTag(wizardContext.image ?? '')) {
promptSteps.push(new IngressPromptStep());
}
promptSteps.push(new ContainerAppOverwriteConfirmStep());

const wizard: AzureWizard<DeployImageApiContext> = new AzureWizard(wizardContext, {
title: localize('deploy', 'Deploy image to container app "{0}"', containerApp.name),
promptSteps,
executeSteps,
executeSteps: [
getVerifyProvidersStep<DeployImageApiContext>(),
new ContainerAppUpdateStep(),
],
showLoadingPrompt: true
});

Expand Down
21 changes: 20 additions & 1 deletion src/commands/image/imageSource/ContainerAppUpdateStep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,41 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { type Ingress } from "@azure/arm-appcontainers";
import { AzureWizardExecuteStep, GenericParentTreeItem, GenericTreeItem, activityFailContext, activityFailIcon, activitySuccessContext, activitySuccessIcon, createUniversallyUniqueContextValue, nonNullProp, type ExecuteActivityOutput } from "@microsoft/vscode-azext-utils";
import { type Progress } from "vscode";
import { ext } from "../../../extensionVariables";
import { getContainerEnvelopeWithSecrets, type ContainerAppModel } from "../../../tree/ContainerAppItem";
import { localize } from "../../../utils/localize";
import { type IngressContext } from "../../ingress/IngressContext";
import { enabledIngressDefaults } from "../../ingress/enableIngress/EnableIngressStep";
import { updateContainerApp } from "../../updateContainerApp";
import { type ImageSourceContext } from "./ImageSourceContext";
import { getContainerNameForImage } from "./containerRegistry/getContainerNameForImage";

export class ContainerAppUpdateStep<T extends ImageSourceContext> extends AzureWizardExecuteStep<T> {
export class ContainerAppUpdateStep<T extends ImageSourceContext & IngressContext> extends AzureWizardExecuteStep<T> {
public priority: number = 680;

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

let ingress: Ingress | undefined;
if (context.enableIngress) {
ingress = {
...enabledIngressDefaults,
...containerAppEnvelope.configuration.ingress ?? {}, // Overwrite any default settings if we already have previous configurations set
external: context.enableExternal ?? containerAppEnvelope.configuration.ingress?.external,
targetPort: context.targetPort ?? containerAppEnvelope.configuration.ingress?.targetPort,
};
} else if (context.enableIngress === false) {
ingress = undefined;
} else {
// If enableIngress is not set, just default to the previous settings if they exist
ingress = containerAppEnvelope.configuration.ingress;
}

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

Expand Down
33 changes: 18 additions & 15 deletions src/commands/ingress/IngressPromptStep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,28 +59,31 @@ export async function tryConfigureIngressUsingDockerfile(context: IngressContext
return;
}

if (!context.dockerfileExposePorts) {
context.enableIngress = false;
context.enableExternal = false;
} else if (context.dockerfileExposePorts) {
if (context.dockerfileExposePorts) {
context.enableIngress = true;
context.enableExternal = true;
context.targetPort = getDefaultPort(context);
} else {
context.enableIngress = false;
context.enableExternal = false;
}

// If a container app already exists, activity children will be added automatically in later execute steps
if (!context.containerApp) {
context.activityChildren?.push(
new GenericTreeItem(undefined, {
contextValue: createUniversallyUniqueContextValue(['ingressPromptStepSuccessItem', activitySuccessContext]),
label: context.enableIngress ?
localize('ingressEnableLabel', 'Enable ingress on port {0} (from Dockerfile configuration)', context.targetPort) :
localize('ingressDisableLabel', 'Disable ingress (from Dockerfile configuration)'),
iconPath: activitySuccessIcon
})
);
const currentExternalEnabled: boolean | undefined = context.containerApp?.configuration?.ingress?.external;
const currentTargetPort: number | undefined = context.containerApp?.configuration?.ingress?.targetPort;
if (currentExternalEnabled === context.enableExternal && currentTargetPort === context.targetPort) {
return;
}

context.activityChildren?.push(
new GenericTreeItem(undefined, {
contextValue: createUniversallyUniqueContextValue(['ingressPromptStepSuccessItem', activitySuccessContext]),
label: context.enableIngress ?
localize('ingressEnableLabel', 'Enable ingress on port {0} (from Dockerfile configuration)', context.targetPort) :
localize('ingressDisableLabel', 'Disable ingress (from Dockerfile configuration)'),
iconPath: activitySuccessIcon
})
);

ext.outputChannel.appendLog(context.enableIngress ?
localize('ingressEnabledLabel', 'Detected ingress on port {0} using Dockerfile configuration.', context.targetPort) :
localize('ingressDisabledLabel', 'Detected no ingress using Dockerfile configuration.')
Expand Down
23 changes: 14 additions & 9 deletions src/commands/ingress/enableIngress/EnableIngressStep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@ import { localize } from "../../../utils/localize";
import { updateContainerApp } from "../../updateContainerApp";
import { type IngressBaseContext } from "../IngressContext";

export const enabledIngressDefaults = {
transport: 'auto',
allowInsecure: false,
traffic: [
{
weight: 100,
latestRevision: true
}
],
};

export class EnableIngressStep extends AzureWizardExecuteStep<IngressBaseContext> {
public priority: number = 750;

Expand All @@ -18,19 +29,13 @@ export class EnableIngressStep extends AzureWizardExecuteStep<IngressBaseContext

const containerApp = nonNullProp(context, 'containerApp');
const ingress: Ingress = {
...enabledIngressDefaults,
...containerApp.configuration?.ingress ?? {},
targetPort: context.targetPort,
external: context.enableExternal,
transport: 'auto',
allowInsecure: false,
traffic: [
{
weight: 100,
latestRevision: true
}
],
}

await updateContainerApp(context, context.subscription, containerApp, { configuration: { ingress: ingress as Ingress | undefined } });
context.containerApp = await updateContainerApp(context, context.subscription, containerApp, { configuration: { ingress: ingress as Ingress | undefined } });
}

public shouldExecute(context: IngressBaseContext): boolean {
Expand Down
4 changes: 4 additions & 0 deletions src/utils/imageNameUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ export function parseImageName(imageName?: string): ParsedImageName {
};
}

export function getImageNameWithoutTag(imageName: string): string {
return imageName.replace(/:[^:]*$/, '');
}

/**
* @param registryName When parsed from a full image name, everything before the first slash
*/
Expand Down
61 changes: 61 additions & 0 deletions test/imageNameUtils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as assert from "assert";
import { getImageNameWithoutTag } from "../extension.bundle";

suite('imageNameUtils', () => {
test('getImageNameWithoutTag', () => {
const testValues: string[] = [
'',
'myimage:latest',
'repository/name:1.0.0',
'custom-registry.com/myimage:v2',
'library/ubuntu:20.04',
'project/service-name:dev-build',
'docker.io/library/nginx:stable',
'my-repo/my-service:release-candidate',
'anotherrepo/anotherimage:test',
'image_without_tag',
'my-image:with:multiple:colons',
'some-registry.io/nested/repo/image:12345',
'edgecase-without-tag:',
'dockerhub.com/image-name:alpha-beta',
'registry.example.com:5000/repo/image:tagname',
'private-repo/myimage:',
'test-image-with-special-characters:beta@123',
'path/to/image:no-colon-in-name',
'simple-image:another:tag',
'example.com:8080/repo/image:v3'
];

const expectedValues: string[] = [
'',
'myimage',
'repository/name',
'custom-registry.com/myimage',
'library/ubuntu',
'project/service-name',
'docker.io/library/nginx',
'my-repo/my-service',
'anotherrepo/anotherimage',
'image_without_tag',
'my-image:with:multiple',
'some-registry.io/nested/repo/image',
'edgecase-without-tag',
'dockerhub.com/image-name',
'registry.example.com:5000/repo/image',
'private-repo/myimage',
'test-image-with-special-characters',
'path/to/image',
'simple-image:another',
'example.com:8080/repo/image'
];

for (let i = 0; i < testValues.length; i++) {
assert.equal(getImageNameWithoutTag(testValues[i]), expectedValues[i]);
}
});
});