Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a27a2bc
Partial
MicroFish91 Sep 1, 2023
42d6c19
Merge branch 'main' of https://github.com/microsoft/vscode-azureconta…
MicroFish91 Sep 6, 2023
c13c61e
Update validation logic
MicroFish91 Sep 6, 2023
0e25f45
Wip
MicroFish91 Sep 6, 2023
1134743
Wip tests
MicroFish91 Sep 7, 2023
426b5e4
Add tests
MicroFish91 Sep 8, 2023
64dccfc
Revert main.js
MicroFish91 Sep 8, 2023
fbffd33
Update test file names
MicroFish91 Sep 8, 2023
0fa2f4c
Import type
MicroFish91 Sep 8, 2023
91eff67
Update test case
MicroFish91 Sep 8, 2023
06a7ba9
Make pr smaller
MicroFish91 Sep 8, 2023
d21cdb2
Fix
MicroFish91 Sep 8, 2023
9e8f37f
Impl
MicroFish91 Sep 8, 2023
d8d2345
Merge with part I
MicroFish91 Sep 8, 2023
a9ab3ac
Add back name validation refactors
MicroFish91 Sep 8, 2023
dae4d98
Update var names
MicroFish91 Sep 8, 2023
830ecb5
Revert old change
MicroFish91 Sep 8, 2023
3e8ea38
nit
MicroFish91 Sep 8, 2023
57e86fe
Impl
MicroFish91 Sep 11, 2023
3405415
Combine if statements
MicroFish91 Sep 11, 2023
0ff2e01
Import type
MicroFish91 Sep 11, 2023
d689816
Update test display
MicroFish91 Sep 11, 2023
a721f97
Misc
MicroFish91 Sep 12, 2023
0f86bdc
Add additional parse notes
MicroFish91 Sep 13, 2023
4cab767
Update var name
MicroFish91 Sep 13, 2023
fe4f955
Merge branch 'mwf/deployWorkspaceProject-II' of https://github.com/mi…
MicroFish91 Sep 13, 2023
de11fc0
Updates
MicroFish91 Sep 13, 2023
3a3942c
Merge with main
MicroFish91 Sep 15, 2023
7a236f0
Update microsoft header
MicroFish91 Sep 15, 2023
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
4 changes: 4 additions & 0 deletions extension.bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ export * from '@microsoft/vscode-azext-utils';
// Export activate/deactivate for main.js
export * from './src/commands/deployWorkspaceProject/DeployWorkspaceProjectContext';
export * from './src/commands/deployWorkspaceProject/getDefaultValues/DefaultResourcesNameStep';
export * from './src/commands/ingress/IngressContext';
export * from './src/commands/ingress/IngressPromptStep';
export * from './src/commands/ingress/editTargetPort/getDefaultPort';
export * from './src/commands/ingress/tryGetDockerfileExposePorts';
export { activate, deactivate } from './src/extension';
export * from './src/extensionVariables';
export * from './src/utils/validateUtils';
Expand Down
7 changes: 7 additions & 0 deletions gulpfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ async function cleanReadme(): Promise<void> {
await fse.writeFile(readmePath, data);
}

async function preTest(): Promise<void> {
const fromPath: string = path.join(__dirname, 'test', 'ingress', 'dockerfileSamples');
const toPath: string = path.join(__dirname, 'dist', 'test', 'ingress', 'dockerfileSamples');
fse.copySync(fromPath, toPath);
}

exports['webpack-dev'] = gulp.series(prepareForWebpack, () => gulp_webpack('development'));
exports['webpack-prod'] = gulp.series(prepareForWebpack, () => gulp_webpack('production'));
exports.cleanReadme = cleanReadme;
exports.preTest = preTest;
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -507,7 +507,8 @@
"package": "vsce package --githubBranch main --no-dependencies",
"lint": "eslint --ext .ts .",
"lint-fix": "eslint --ext .ts . --fix",
"test": "node ./out/test/runTest.js",
"pretest": "gulp preTest",
"test": "node ./dist/test/runTest.js",
"webpack": "tsc && gulp webpack-dev",
"webpack-profile": "webpack --profile --json --mode production > webpack-stats.json && echo Use http://webpack.github.io/analyse to analyze the stats",
"prepare": "husky install"
Expand Down
1 change: 1 addition & 0 deletions src/commands/createContainerApp/createContainerApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export async function createContainerApp(context: IActionContext & Partial<ICrea
...(await createActivityContext()),
subscription: node.subscription,
managedEnvironmentId: node.managedEnvironment.id,
alwaysPromptIngress: true
};

const title: string = localize('createContainerApp', 'Create Container App');
Expand Down
6 changes: 6 additions & 0 deletions src/commands/ingress/IngressContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,16 @@

import type { ExecuteActivityContext } from "@microsoft/vscode-azext-utils";
import type { IContainerAppContext } from "../IContainerAppContext";
import { PortRange } from "./tryGetDockerfileExposePorts";

export interface IngressContext extends IContainerAppContext, ExecuteActivityContext {
enableIngress?: boolean;
enableExternal?: boolean;

targetPort?: number;

// For detecting an expose port using a workspace Dockerfile
dockerfilePath?: string;
dockerfileExposePorts?: PortRange[];
alwaysPromptIngress?: boolean;
}
46 changes: 46 additions & 0 deletions src/commands/ingress/IngressPromptStep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,26 @@
*--------------------------------------------------------------------------------------------*/

import { AzureWizardExecuteStep, AzureWizardPromptStep, IWizardOptions } from "@microsoft/vscode-azext-utils";
import { ext } from "../../extensionVariables";
import { localize } from "../../utils/localize";
import type { IngressContext } from "./IngressContext";
import { DisableIngressStep } from "./disableIngress/DisableIngressStep";
import { TargetPortInputStep } from "./editTargetPort/TargetPortInputStep";
import { getDefaultPort } from "./editTargetPort/getDefaultPort";
import { EnableIngressStep } from "./enableIngress/EnableIngressStep";
import { IngressVisibilityStep } from "./enableIngress/IngressVisibilityStep";
import { tryGetDockerfileExposePorts } from "./tryGetDockerfileExposePorts";

export class IngressPromptStep extends AzureWizardPromptStep<IngressContext> {
public async prompt(context: IngressContext): Promise<void> {
context.enableIngress = (await context.ui.showQuickPick([{ label: localize('enable', 'Enable'), data: true }, { label: localize('disable', 'Disable'), data: false }],
{ placeHolder: localize('enableIngress', 'Enable ingress for applications that need an HTTP endpoint.') })).data;
}

public async configureBeforePrompt(context: IngressContext): Promise<void> {
await tryConfigureIngressUsingDockerfile(context);
}

public shouldPrompt(context: IngressContext): boolean {
return context.enableIngress === undefined;
}
Expand All @@ -37,3 +44,42 @@ export class IngressPromptStep extends AzureWizardPromptStep<IngressContext> {
return { promptSteps, executeSteps };
}
}

export async function tryConfigureIngressUsingDockerfile(context: IngressContext): Promise<void> {
if (!context.dockerfilePath) {
return;
}

context.dockerfileExposePorts = await tryGetDockerfileExposePorts(context.dockerfilePath);

if (context.alwaysPromptIngress) {
return;
}

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

// 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: createActivityChildContext(['ingressPromptStep', activitySuccessContext]),
// label: context.enableIngress ?
// localize('ingressEnableLabel', 'Enable ingress on port {0} (found Dockerfile configuration)', context.targetPort) :
// localize('ingressDisableLabel', 'Disable ingress (found 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.')
);
}
14 changes: 13 additions & 1 deletion src/commands/ingress/editTargetPort/getDefaultPort.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,17 @@
import type { IngressContext } from "../IngressContext";

export function getDefaultPort(context: IngressContext, fallbackPort: number = 80): number {
return context.containerApp?.configuration?.ingress?.targetPort || fallbackPort;
const currentDeploymentPort: number | undefined = context.containerApp?.configuration?.ingress?.targetPort;

let dockerfilePortSuggestion: number | undefined;
if (
// If there's already a deployment port, don't suggest a new port if it's already a port within range of the current Dockerfile expose ports
(currentDeploymentPort && context.dockerfileExposePorts && !context.dockerfileExposePorts.some(p => p.includes(currentDeploymentPort))) ||
// If no deployment port but we found expose ports
(!currentDeploymentPort && context.dockerfileExposePorts)
) {
dockerfilePortSuggestion = context.dockerfileExposePorts[0].start;
}

return dockerfilePortSuggestion || currentDeploymentPort || fallbackPort;
}
62 changes: 62 additions & 0 deletions src/commands/ingress/tryGetDockerfileExposePorts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { AzExtFsExtra } from "@microsoft/vscode-azext-utils";

export async function tryGetDockerfileExposePorts(dockerfilePath: string): Promise<PortRange[] | undefined> {
if (!await AzExtFsExtra.pathExists(dockerfilePath)) {
return undefined;
}

const content: string = await AzExtFsExtra.readFile(dockerfilePath);
const lines: string[] = content.split('\n');

const portRanges: PortRange[] = [];
for (const line of lines) {
if (!/^EXPOSE/i.test(line.trim())) {
continue;
}

// Identify all single port numbers that aren't for udp
// Example formats: `3000` or `3000/tcp` but not `3000/udp`
// Note: (?<=\s) prevents the last number in a range 3000-3010 from being selected
const singlePorts: string[] = line.match(/(?<=\s)\d{2,5}(?!(\-)|(\/udp))\b/g) ?? [];
for (const sp of singlePorts) {
portRanges.push(new PortRange(parseInt(sp)));
}

// Identify all port ranges
// Example format: `3000-3010`
const portRange: string[] = line.match(/\d{2,5}\-\d{2,5}/g) ?? [];
for (const pr of portRange) {
const [start, end] = pr.split('-');
portRanges.push(new PortRange(parseInt(start), parseInt(end)));
}
}

return portRanges.length ? portRanges : undefined;
}

export class PortRange {
private readonly _start: number;
private readonly _end: number;

constructor(start: number, end?: number) {
this._start = start;
this._end = end ? end : start;
}

get start(): number {
return this._start;
}

get end(): number {
return this._end;
}

includes(port: number): boolean {
return port >= this.start && port <= this.end;
}
}
49 changes: 49 additions & 0 deletions test/ingress/IngressPromptStep.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { AzExtFsExtra } from "@microsoft/vscode-azext-utils";
import * as assert from "assert";
import * as path from "path";
import { IngressContext, tryConfigureIngressUsingDockerfile } from "../../extension.bundle";
import type { MockIngressContext } from "./MockIngressContext";
import { expectedSamplePorts } from "./tryGetDockerfileExposePorts.test";

suite('IngressPromptStep', async () => {
test('tryConfigureIngressUsingDockerfile', async () => {
const dockerfileSamplesPath: string = path.join(__dirname, 'dockerfileSamples');
const dockerfileSamples = await AzExtFsExtra.readDirectory(dockerfileSamplesPath);

const expectedResult: MockIngressContext[] = [
{ enableIngress: true, enableExternal: true, dockerfileExposePorts: expectedSamplePorts[0], targetPort: 443 },
{ enableIngress: undefined, enableExternal: undefined, dockerfileExposePorts: undefined, targetPort: undefined }, // no dockerfilePath
{ enableIngress: true, enableExternal: true, dockerfileExposePorts: expectedSamplePorts[2], targetPort: 80 },
{ enableIngress: undefined, enableExternal: undefined, dockerfileExposePorts: expectedSamplePorts[3], targetPort: undefined }, // alwaysPromptIngress=true
{ enableIngress: true, enableExternal: true, dockerfileExposePorts: expectedSamplePorts[4], targetPort: 443 },
{ enableIngress: true, enableExternal: true, dockerfileExposePorts: expectedSamplePorts[5], targetPort: 80 },
{ enableIngress: false, enableExternal: false, dockerfileExposePorts: undefined, targetPort: undefined }, // no expose
];

for (const [i, ds] of dockerfileSamples.entries()) {
const context: MockIngressContext = {
dockerfilePath: i === 1 ? undefined : ds.fsPath,
alwaysPromptIngress: i === 3
};

await tryConfigureIngressUsingDockerfile(context as IngressContext);

assert.deepStrictEqual({
enableIngress: context.enableIngress,
enableExternal: context.enableExternal,
dockerfileExposePortsLength: context.dockerfileExposePorts?.length,
targetPort: context.targetPort
}, {
enableIngress: expectedResult[i].enableIngress,
enableExternal: expectedResult[i].enableExternal,
dockerfileExposePortsLength: expectedResult[i].dockerfileExposePorts?.length,
targetPort: expectedResult[i].targetPort
});
}
});
});
19 changes: 19 additions & 0 deletions test/ingress/MockIngressContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import type { PortRange } from "../../extension.bundle";

export interface MockIngressContext {
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.

Just want to confirm, but the reason you're not just using the IngressContext because of the containerApp has too many required properties that aren't worth mocking?

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.

Yeah, that's a big part of the reason

containerApp?: { configuration: { ingress: { targetPort: number } } };

enableIngress?: boolean;
enableExternal?: boolean;

targetPort?: number;

dockerfilePath?: string;
dockerfileExposePorts?: PortRange[];
alwaysPromptIngress?: boolean;
}
7 changes: 7 additions & 0 deletions test/ingress/dockerfileSamples/sample1.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
## Modified example from Azure-Samples/acr-build-helloworld-node
FROM node:lts-alpine

COPY . /src
RUN cd /src && npm install
EXPOSE 443 80
CMD ["node", "/src/server.js"]
8 changes: 8 additions & 0 deletions test/ingress/dockerfileSamples/sample2.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
## Modified example from Azure-Samples/acr-build-helloworld-node
FROM node:lts-alpine

COPY . /src
RUN cd /src && npm install
EXPOSE 80
EXPOSE 443
CMD ["node", "/src/server.js"]
7 changes: 7 additions & 0 deletions test/ingress/dockerfileSamples/sample3.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
## Modified example from Azure-Samples/acr-build-helloworld-node
FROM node:lts-alpine

COPY . /src
RUN cd /src && npm install
EXPOSE 80 8080-8090
CMD ["node", "/src/server.js"]
7 changes: 7 additions & 0 deletions test/ingress/dockerfileSamples/sample4.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
## Modified example from Azure-Samples/acr-build-helloworld-node
FROM node:lts-alpine

COPY . /src
RUN cd /src && npm install
EXPOSE 8080-8090 80
CMD ["node", "/src/server.js"]
8 changes: 8 additions & 0 deletions test/ingress/dockerfileSamples/sample5.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
## Modified example from Azure-Samples/acr-build-helloworld-node
FROM node:lts-alpine

COPY . /src
RUN cd /src && npm install
EXPOSE 443/tcp
EXPOSE 5000/udp
CMD ["node", "/src/server.js"]
9 changes: 9 additions & 0 deletions test/ingress/dockerfileSamples/sample6.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
## Modified example from Azure-Samples/acr-build-helloworld-node
FROM node:lts-alpine

COPY . /src
RUN cd /src && npm install

## Extra unrealistic expose scenario used to double-check formatting logic
EXPOSE 80 443/tcp 8080-8090 5000/udp
CMD ["node", "/src/server.js"]
7 changes: 7 additions & 0 deletions test/ingress/dockerfileSamples/sample7.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
## Modified example from Azure-Samples/acr-build-helloworld-node
FROM node:lts-alpine

COPY . /src
RUN cd /src && npm install

CMD ["node", "/src/server.js"]
36 changes: 36 additions & 0 deletions test/ingress/getDefaultPort.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*---------------------------------------------------------------------------------------------
* 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 { IngressContext, PortRange, getDefaultPort } from "../../extension.bundle";
import type { MockIngressContext } from "./MockIngressContext";

suite('getDefaultPort', async () => {
test('Correctly suggests a new port when Dockerfile expose ports are detected with no existing container app port', async () => {
const context: MockIngressContext = {
dockerfileExposePorts: [new PortRange(443), new PortRange(8080, 8090)]
};
assert.equal(getDefaultPort(context as IngressContext), 443);
});

test('Correctly suggests deployed port when Dockerfile expose ports are detected that overlap with existing container app port', async () => {
const context: MockIngressContext = {
containerApp: { configuration: { ingress: { targetPort: 8081 } } },
dockerfileExposePorts: [new PortRange(80), new PortRange(443), new PortRange(8080, 8090)]
};
assert.equal(getDefaultPort(context as IngressContext), 8081);
});

test('Correctly suggests existing deploy port when no expose ports are detected', async () => {
const context: MockIngressContext = {
containerApp: { configuration: { ingress: { targetPort: 3000 } } },
};
assert.equal(getDefaultPort(context as IngressContext), 3000);
});

test('Correctly suggests fallback port when no other ports are available', async () => {
assert.equal(getDefaultPort({} as IngressContext), 80);
});
});
Loading