Skip to content

Commit d28f31b

Browse files
authored
Add Dockerfile port detection to ingress configuration logic (#449)
1 parent 5963a93 commit d28f31b

20 files changed

+340
-2
lines changed

extension.bundle.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ export * from '@microsoft/vscode-azext-utils';
1818
// Export activate/deactivate for main.js
1919
export * from './src/commands/deployWorkspaceProject/DeployWorkspaceProjectContext';
2020
export * from './src/commands/deployWorkspaceProject/getDefaultValues/DefaultResourcesNameStep';
21+
export * from './src/commands/ingress/IngressContext';
22+
export * from './src/commands/ingress/IngressPromptStep';
23+
export * from './src/commands/ingress/editTargetPort/getDefaultPort';
24+
export * from './src/commands/ingress/tryGetDockerfileExposePorts';
2125
export { activate, deactivate } from './src/extension';
2226
export * from './src/extensionVariables';
2327
export * from './src/utils/validateUtils';

gulpfile.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ async function cleanReadme(): Promise<void> {
2626
await fse.writeFile(readmePath, data);
2727
}
2828

29+
async function preTest(): Promise<void> {
30+
const fromPath: string = path.join(__dirname, 'test', 'ingress', 'dockerfileSamples');
31+
const toPath: string = path.join(__dirname, 'dist', 'test', 'ingress', 'dockerfileSamples');
32+
fse.copySync(fromPath, toPath);
33+
}
34+
2935
exports['webpack-dev'] = gulp.series(prepareForWebpack, () => gulp_webpack('development'));
3036
exports['webpack-prod'] = gulp.series(prepareForWebpack, () => gulp_webpack('production'));
3137
exports.cleanReadme = cleanReadme;
38+
exports.preTest = preTest;

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -507,7 +507,8 @@
507507
"package": "vsce package --githubBranch main --no-dependencies",
508508
"lint": "eslint --ext .ts .",
509509
"lint-fix": "eslint --ext .ts . --fix",
510-
"test": "node ./out/test/runTest.js",
510+
"pretest": "gulp preTest",
511+
"test": "node ./dist/test/runTest.js",
511512
"webpack": "tsc && gulp webpack-dev",
512513
"webpack-profile": "webpack --profile --json --mode production > webpack-stats.json && echo Use http://webpack.github.io/analyse to analyze the stats",
513514
"prepare": "husky install"

src/commands/createContainerApp/createContainerApp.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export async function createContainerApp(context: IActionContext & Partial<ICrea
3030
...(await createActivityContext()),
3131
subscription: node.subscription,
3232
managedEnvironmentId: node.managedEnvironment.id,
33+
alwaysPromptIngress: true
3334
};
3435

3536
const title: string = localize('createContainerApp', 'Create Container App');

src/commands/ingress/IngressContext.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,16 @@
55

66
import type { ExecuteActivityContext } from "@microsoft/vscode-azext-utils";
77
import type { IContainerAppContext } from "../IContainerAppContext";
8+
import { PortRange } from "./tryGetDockerfileExposePorts";
89

910
export interface IngressContext extends IContainerAppContext, ExecuteActivityContext {
1011
enableIngress?: boolean;
1112
enableExternal?: boolean;
1213

1314
targetPort?: number;
15+
16+
// For detecting an expose port using a workspace Dockerfile
17+
dockerfilePath?: string;
18+
dockerfileExposePorts?: PortRange[];
19+
alwaysPromptIngress?: boolean;
1420
}

src/commands/ingress/IngressPromptStep.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,26 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { AzureWizardExecuteStep, AzureWizardPromptStep, IWizardOptions } from "@microsoft/vscode-azext-utils";
7+
import { ext } from "../../extensionVariables";
78
import { localize } from "../../utils/localize";
89
import type { IngressContext } from "./IngressContext";
910
import { DisableIngressStep } from "./disableIngress/DisableIngressStep";
1011
import { TargetPortInputStep } from "./editTargetPort/TargetPortInputStep";
12+
import { getDefaultPort } from "./editTargetPort/getDefaultPort";
1113
import { EnableIngressStep } from "./enableIngress/EnableIngressStep";
1214
import { IngressVisibilityStep } from "./enableIngress/IngressVisibilityStep";
15+
import { tryGetDockerfileExposePorts } from "./tryGetDockerfileExposePorts";
1316

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

23+
public async configureBeforePrompt(context: IngressContext): Promise<void> {
24+
await tryConfigureIngressUsingDockerfile(context);
25+
}
26+
2027
public shouldPrompt(context: IngressContext): boolean {
2128
return context.enableIngress === undefined;
2229
}
@@ -37,3 +44,42 @@ export class IngressPromptStep extends AzureWizardPromptStep<IngressContext> {
3744
return { promptSteps, executeSteps };
3845
}
3946
}
47+
48+
export async function tryConfigureIngressUsingDockerfile(context: IngressContext): Promise<void> {
49+
if (!context.dockerfilePath) {
50+
return;
51+
}
52+
53+
context.dockerfileExposePorts = await tryGetDockerfileExposePorts(context.dockerfilePath);
54+
55+
if (context.alwaysPromptIngress) {
56+
return;
57+
}
58+
59+
if (!context.dockerfileExposePorts) {
60+
context.enableIngress = false;
61+
context.enableExternal = false;
62+
} else if (context.dockerfileExposePorts) {
63+
context.enableIngress = true;
64+
context.enableExternal = true;
65+
context.targetPort = getDefaultPort(context);
66+
}
67+
68+
// If a container app already exists, activity children will be added automatically in later execute steps
69+
// if (!context.containerApp) {
70+
// context.activityChildren?.push(
71+
// new GenericTreeItem(undefined, {
72+
// contextValue: createActivityChildContext(['ingressPromptStep', activitySuccessContext]),
73+
// label: context.enableIngress ?
74+
// localize('ingressEnableLabel', 'Enable ingress on port {0} (found Dockerfile configuration)', context.targetPort) :
75+
// localize('ingressDisableLabel', 'Disable ingress (found Dockerfile configuration)'),
76+
// iconPath: activitySuccessIcon
77+
// })
78+
// );
79+
// }
80+
81+
ext.outputChannel.appendLog(context.enableIngress ?
82+
localize('ingressEnabledLabel', 'Detected ingress on port {0} using Dockerfile configuration.', context.targetPort) :
83+
localize('ingressDisabledLabel', 'Detected no ingress using Dockerfile configuration.')
84+
);
85+
}

src/commands/ingress/editTargetPort/getDefaultPort.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,17 @@
66
import type { IngressContext } from "../IngressContext";
77

88
export function getDefaultPort(context: IngressContext, fallbackPort: number = 80): number {
9-
return context.containerApp?.configuration?.ingress?.targetPort || fallbackPort;
9+
const currentDeploymentPort: number | undefined = context.containerApp?.configuration?.ingress?.targetPort;
10+
11+
let dockerfilePortSuggestion: number | undefined;
12+
if (
13+
// 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
14+
(currentDeploymentPort && context.dockerfileExposePorts && !context.dockerfileExposePorts.some(p => p.includes(currentDeploymentPort))) ||
15+
// If no deployment port but we found expose ports
16+
(!currentDeploymentPort && context.dockerfileExposePorts)
17+
) {
18+
dockerfilePortSuggestion = context.dockerfileExposePorts[0].start;
19+
}
20+
21+
return dockerfilePortSuggestion || currentDeploymentPort || fallbackPort;
1022
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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 { AzExtFsExtra } from "@microsoft/vscode-azext-utils";
7+
8+
export async function tryGetDockerfileExposePorts(dockerfilePath: string): Promise<PortRange[] | undefined> {
9+
if (!await AzExtFsExtra.pathExists(dockerfilePath)) {
10+
return undefined;
11+
}
12+
13+
const content: string = await AzExtFsExtra.readFile(dockerfilePath);
14+
const lines: string[] = content.split('\n');
15+
16+
const portRanges: PortRange[] = [];
17+
for (const line of lines) {
18+
if (!/^EXPOSE/i.test(line.trim())) {
19+
continue;
20+
}
21+
22+
// Identify all single port numbers that aren't for udp
23+
// Example formats: `3000` or `3000/tcp` but not `3000/udp`
24+
// Note: (?<=\s) prevents the last number in a range 3000-3010 from being selected
25+
const singlePorts: string[] = line.match(/(?<=\s)\d{2,5}(?!(\-)|(\/udp))\b/g) ?? [];
26+
for (const sp of singlePorts) {
27+
portRanges.push(new PortRange(parseInt(sp)));
28+
}
29+
30+
// Identify all port ranges
31+
// Example format: `3000-3010`
32+
const portRange: string[] = line.match(/\d{2,5}\-\d{2,5}/g) ?? [];
33+
for (const pr of portRange) {
34+
const [start, end] = pr.split('-');
35+
portRanges.push(new PortRange(parseInt(start), parseInt(end)));
36+
}
37+
}
38+
39+
return portRanges.length ? portRanges : undefined;
40+
}
41+
42+
export class PortRange {
43+
private readonly _start: number;
44+
private readonly _end: number;
45+
46+
constructor(start: number, end?: number) {
47+
this._start = start;
48+
this._end = end ? end : start;
49+
}
50+
51+
get start(): number {
52+
return this._start;
53+
}
54+
55+
get end(): number {
56+
return this._end;
57+
}
58+
59+
includes(port: number): boolean {
60+
return port >= this.start && port <= this.end;
61+
}
62+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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 { AzExtFsExtra } from "@microsoft/vscode-azext-utils";
7+
import * as assert from "assert";
8+
import * as path from "path";
9+
import { IngressContext, tryConfigureIngressUsingDockerfile } from "../../extension.bundle";
10+
import type { MockIngressContext } from "./MockIngressContext";
11+
import { expectedSamplePorts } from "./tryGetDockerfileExposePorts.test";
12+
13+
suite('IngressPromptStep', async () => {
14+
test('tryConfigureIngressUsingDockerfile', async () => {
15+
const dockerfileSamplesPath: string = path.join(__dirname, 'dockerfileSamples');
16+
const dockerfileSamples = await AzExtFsExtra.readDirectory(dockerfileSamplesPath);
17+
18+
const expectedResult: MockIngressContext[] = [
19+
{ enableIngress: true, enableExternal: true, dockerfileExposePorts: expectedSamplePorts[0], targetPort: 443 },
20+
{ enableIngress: undefined, enableExternal: undefined, dockerfileExposePorts: undefined, targetPort: undefined }, // no dockerfilePath
21+
{ enableIngress: true, enableExternal: true, dockerfileExposePorts: expectedSamplePorts[2], targetPort: 80 },
22+
{ enableIngress: undefined, enableExternal: undefined, dockerfileExposePorts: expectedSamplePorts[3], targetPort: undefined }, // alwaysPromptIngress=true
23+
{ enableIngress: true, enableExternal: true, dockerfileExposePorts: expectedSamplePorts[4], targetPort: 443 },
24+
{ enableIngress: true, enableExternal: true, dockerfileExposePorts: expectedSamplePorts[5], targetPort: 80 },
25+
{ enableIngress: false, enableExternal: false, dockerfileExposePorts: undefined, targetPort: undefined }, // no expose
26+
];
27+
28+
for (const [i, ds] of dockerfileSamples.entries()) {
29+
const context: MockIngressContext = {
30+
dockerfilePath: i === 1 ? undefined : ds.fsPath,
31+
alwaysPromptIngress: i === 3
32+
};
33+
34+
await tryConfigureIngressUsingDockerfile(context as IngressContext);
35+
36+
assert.deepStrictEqual({
37+
enableIngress: context.enableIngress,
38+
enableExternal: context.enableExternal,
39+
dockerfileExposePortsLength: context.dockerfileExposePorts?.length,
40+
targetPort: context.targetPort
41+
}, {
42+
enableIngress: expectedResult[i].enableIngress,
43+
enableExternal: expectedResult[i].enableExternal,
44+
dockerfileExposePortsLength: expectedResult[i].dockerfileExposePorts?.length,
45+
targetPort: expectedResult[i].targetPort
46+
});
47+
}
48+
});
49+
});

test/ingress/MockIngressContext.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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 { PortRange } from "../../extension.bundle";
7+
8+
export interface MockIngressContext {
9+
containerApp?: { configuration: { ingress: { targetPort: number } } };
10+
11+
enableIngress?: boolean;
12+
enableExternal?: boolean;
13+
14+
targetPort?: number;
15+
16+
dockerfilePath?: string;
17+
dockerfileExposePorts?: PortRange[];
18+
alwaysPromptIngress?: boolean;
19+
}

0 commit comments

Comments
 (0)