Skip to content

Commit a3105d1

Browse files
committed
feat(config): add new dev-proxy cmd to fec bin
This adds a new "dev-proxy" command to the FEC binary in the config package. This new command uses the new and better frontend-development-proxy which is based on Caddy, where the main benefits alongside speed are HTTP2 support and better websocket support. The new command imitates the behavior of the dev command as much as possible so it should be a drop-in replacement for the consumers.
1 parent 3caae14 commit a3105d1

File tree

7 files changed

+475
-38
lines changed

7 files changed

+475
-38
lines changed

packages/config/src/bin/common.ts

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import { LogType, fecLogger } from '@redhat-cloud-services/frontend-components-config-utilities';
1+
import inquirer from 'inquirer';
2+
import { FrontendCRD } from '@redhat-cloud-services/frontend-components-config-utilities/feo/feo-types';
3+
import { fecLogger, LogType } from '@redhat-cloud-services/frontend-components-config-utilities';
4+
import { hasFEOFeaturesEnabled, readFrontendCRD } from '@redhat-cloud-services/frontend-components-config-utilities/feo/crd-check';
25

36
const { resolve } = require('path');
47
const { statSync } = require('fs');
@@ -45,5 +48,63 @@ export function getWebpackConfigPath(path: string, cwd: string) {
4548
}
4649
}
4750

51+
export async function setEnv(cwd: string) {
52+
return inquirer
53+
.prompt([
54+
{
55+
type: 'list',
56+
name: 'clouddotEnv',
57+
message: 'Which platform environment you want to use?',
58+
choices: ['stage', 'prod', 'dev', 'ephemeral'],
59+
},
60+
])
61+
.then(async (answers) => {
62+
const { clouddotEnv } = answers;
63+
64+
if (clouddotEnv === 'ephemeral') {
65+
const answer = await inquirer.prompt([
66+
{
67+
type: 'input',
68+
name: 'clouddotEnv',
69+
message: 'Please provide the gateway route of your ephemeral environment:',
70+
},
71+
]);
72+
process.env.EPHEMERAL_TARGET = answer.clouddotEnv;
73+
}
74+
process.env.CLOUDOT_ENV = clouddotEnv ? clouddotEnv : 'stage';
75+
process.env.FEC_ROOT_DIR = cwd;
76+
});
77+
}
78+
79+
export function getCdnPath(fecConfig: any, webpackConfig: any, cwd: string): string {
80+
let cdnPath: string;
81+
const { insights } = require(`${cwd}/package.json`);
82+
const frontendCRDPath = fecConfig.frontendCRDPath ?? `${cwd}/deploy/frontend.yaml`;
83+
const frontendCRDRef: { current?: FrontendCRD } = { current: undefined };
84+
let FEOFeaturesEnabled = false;
85+
86+
try {
87+
frontendCRDRef.current = readFrontendCRD(frontendCRDPath);
88+
FEOFeaturesEnabled = hasFEOFeaturesEnabled(frontendCRDRef.current);
89+
} catch (e) {
90+
fecLogger(
91+
LogType.warn,
92+
`FEO features are not enabled. Unable to find frontend CRD file at ${frontendCRDPath}. If you want FEO features for local development, make sure to have a "deploy/frontend.yaml" file in your project or specify its location via "frontendCRDPath" attribute.`,
93+
);
94+
}
95+
96+
if (FEOFeaturesEnabled && fecConfig.publicPath === 'auto' && frontendCRDRef.current) {
97+
cdnPath = `${frontendCRDRef.current?.objects[0]?.spec.frontend.paths[0]}/`.replace(/\/\//, '/');
98+
} else if (fecConfig.publicPath === 'auto') {
99+
cdnPath = `/${fecConfig.deployment || 'apps'}/${insights.appname}/`;
100+
} else {
101+
cdnPath = webpackConfig.output.publicPath;
102+
}
103+
104+
return cdnPath ?? '';
105+
}
106+
48107
module.exports.validateFECConfig = validateFECConfig;
49108
module.exports.getWebpackConfigPath = getWebpackConfigPath;
109+
module.exports.setEnv = setEnv;
110+
module.exports.getCdnPath = getCdnPath;
Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
import { exec, execSync } from 'child_process';
2+
import concurrently, { Command } from 'concurrently';
3+
import fs from 'fs';
4+
import os from 'os';
5+
import path from 'path';
6+
import treeKill from 'tree-kill';
7+
import { fecLogger as fecLoggerDefault, LogType } from '@redhat-cloud-services/frontend-components-config-utilities';
8+
import { getCdnPath, setEnv, validateFECConfig } from './common';
9+
import serveChrome, { checkContainerRuntime, CONTAINER_NAME as CHROME_CONTAINTER_NAME, ContainerRuntime } from './serve-chrome';
10+
11+
const PROXY_URL = 'http://squid.corp.redhat.com:3128';
12+
const DEFAULT_LOCAL_ROUTE = 'host.docker.internal';
13+
const DEFAULT_CHROME_SERVER_PORT = 9998;
14+
const LATEST_IMAGE_TAG = 'latest';
15+
16+
const DEV_PROXY_CONTAINER_PORT = 1337;
17+
const DEV_PROXY_CONTAINER_NAME = 'frontend-development-proxy';
18+
const DEV_PROXY_IMAGE_REPO = `quay.io/redhat-user-workloads/hcc-platex-services-tenant/${DEV_PROXY_CONTAINER_NAME}`;
19+
20+
let execBin: ContainerRuntime | undefined = undefined;
21+
let debug: boolean = false;
22+
23+
interface RouteConfig {
24+
url: string;
25+
is_chrome?: boolean;
26+
}
27+
28+
function fecLogger(logType: LogType, ...data: any[]) {
29+
if (logType === LogType.debug) {
30+
if (debug) {
31+
fecLoggerDefault(logType, ...data);
32+
}
33+
} else {
34+
fecLoggerDefault(logType, ...data);
35+
}
36+
}
37+
38+
function removeContainer(containerName: string) {
39+
try {
40+
fecLogger(LogType.info, `Removing existing container: ${containerName}`);
41+
execSync(`${execBin} rm ${containerName}`, debug ? { stdio: 'inherit' } : { stdio: [] });
42+
} catch (error) {
43+
fecLogger(LogType.error, `Failed to remove the container: ${containerName}`);
44+
}
45+
}
46+
47+
function stopContainer(containerName: string) {
48+
try {
49+
let isRunning: boolean = true;
50+
try {
51+
execSync(`${execBin} inspect -f '{{.State.Running}}' ${containerName}\n`).toString().trim().toLowerCase() === 'true';
52+
} catch (error) {
53+
isRunning = false;
54+
}
55+
56+
if (isRunning) {
57+
fecLogger(LogType.info, `Stopping container: ${containerName}`);
58+
execSync(`${execBin} stop ${containerName}`, debug ? { stdio: 'inherit' } : { stdio: [] });
59+
}
60+
} catch (error) {
61+
fecLogger(LogType.error, `Failed to stop the container: ${containerName}`);
62+
}
63+
}
64+
65+
function pullImage(containerName: string, repo: string, tag: string) {
66+
fecLogger(LogType.info, `Pulling the container: ${containerName}`);
67+
execSync(`${execBin} pull ${repo}:${tag}`, debug ? { stdio: 'inherit' } : { stdio: [] });
68+
}
69+
70+
function createRoutesConfig(fecConfig: any, cdnPath: string, port: string, SPAFallback: boolean, filename: string = 'routes.json'): string {
71+
let routes: Map<string, RouteConfig> = new Map();
72+
73+
let fecRoutes = fecConfig?.routes || undefined;
74+
if (fecConfig?.routesPath) {
75+
fecRoutes = require(fecConfig.routesPath);
76+
}
77+
if (fecRoutes) {
78+
fecRoutes = fecRoutes?.routes || fecRoutes;
79+
}
80+
const fecRoutesEntries = Object.entries<any>(fecRoutes || {});
81+
const chromeEntry = fecRoutesEntries.find(([_, config]) => !!config?.is_chrome);
82+
83+
if (SPAFallback) {
84+
if (!chromeEntry) {
85+
let chromeHost = process.env.FEC_CHROME_HOST;
86+
if (chromeHost) {
87+
chromeHost = chromeHost.replace(/localhost/, DEFAULT_LOCAL_ROUTE).replace(/127\.0\.0\.1/, DEFAULT_LOCAL_ROUTE);
88+
} else {
89+
chromeHost = DEFAULT_LOCAL_ROUTE;
90+
}
91+
routes.set('/apps/chrome/*', {
92+
url: `${chromeHost}:${process.env.FEC_CHROME_PORT}`,
93+
is_chrome: true,
94+
});
95+
} else if (chromeEntry) {
96+
const [handle, config] = chromeEntry;
97+
if (config?.host) {
98+
const host = config.host.replace(/localhost/, DEFAULT_LOCAL_ROUTE).replace(/127\.0\.0\.1/, DEFAULT_LOCAL_ROUTE);
99+
routes.set(`${handle}*`, {
100+
url: host,
101+
is_chrome: true,
102+
});
103+
}
104+
}
105+
}
106+
107+
routes.set(`${cdnPath}*`, { url: `${DEFAULT_LOCAL_ROUTE}:${port}` });
108+
109+
fecRoutesEntries
110+
.filter(([_, config]) => !config?.is_chrome)
111+
.forEach(([handle, config]) => {
112+
if (config?.host) {
113+
const host = config.host.replace(/localhost/, DEFAULT_LOCAL_ROUTE).replace(/127\.0\.0\.1/, DEFAULT_LOCAL_ROUTE);
114+
routes.set(`${handle}*`, { url: host });
115+
}
116+
});
117+
118+
const tempDir = os.tmpdir();
119+
const tempFilePath = path.join(tempDir, filename);
120+
const jsonContent = JSON.stringify(Object.fromEntries(routes));
121+
console.log(jsonContent);
122+
fs.writeFileSync(tempFilePath, jsonContent, { flag: 'w' });
123+
124+
return tempFilePath;
125+
}
126+
127+
async function configureEnvVars(fecConfig: any, argv: any, cwd: string) {
128+
const clouddotEnvOptions = ['stage', 'prod', 'dev', 'ephemeral'];
129+
if (argv?.clouddotEnv) {
130+
if (!clouddotEnvOptions.includes(argv?.clouddotEnv)) {
131+
throw Error(
132+
`Incorrect argument value:\n--clouddotEnv must be one of: [${clouddotEnvOptions.toString()}]\nRun fec --help for more information.`,
133+
);
134+
}
135+
process.env.HCC_ENV = argv?.clouddotEnv;
136+
} else {
137+
await setEnv(cwd);
138+
process.env.HCC_ENV = process.env.CLOUDOT_ENV;
139+
}
140+
const hccEnvSuffix = process.env.HCC_ENV === 'prod' ? '' : `${process.env.HCC_ENV}.`;
141+
process.env.HCC_ENV_URL = process.env.HCC_ENV === 'ephemeral' ? process.env.EPHEMERAL_TARGET : `https://console.${hccEnvSuffix}redhat.com`;
142+
143+
process.env.FEC_CHROME_HOST = fecConfig?.chromeHost ?? '127.0.0.1';
144+
process.env.FEC_CHROME_PORT = fecConfig?.chromePort ?? DEFAULT_CHROME_SERVER_PORT;
145+
}
146+
147+
async function devProxyScript(
148+
argv: {
149+
chromeServerPort?: number | string;
150+
clouddotEnv?: string;
151+
config?: any;
152+
port?: string;
153+
staticPort?: string;
154+
},
155+
cwd: string,
156+
) {
157+
let SPAFallback = true;
158+
let fecConfig: any = {};
159+
let webpackConfig;
160+
const webpackConfigPath: string =
161+
argv.config || `${cwd}/node_modules/@redhat-cloud-services/frontend-components-config/bin/dev-proxy.webpack.config.js`;
162+
process.env.FEC_DEV_PROXY = 'true';
163+
164+
// Get Configs
165+
try {
166+
validateFECConfig(cwd);
167+
fecConfig = require(process.env.FEC_CONFIG_PATH!);
168+
fecConfig = structuredClone(fecConfig);
169+
debug = fecConfig?.debug ?? false;
170+
} catch (error) {
171+
fecLogger(LogType.error, 'Failed to get the FEC config:', error);
172+
process.exit(1);
173+
}
174+
try {
175+
fs.statSync(webpackConfigPath);
176+
webpackConfig = require(webpackConfigPath);
177+
if (typeof webpackConfig === 'function') {
178+
webpackConfig = webpackConfig(process.env);
179+
}
180+
webpackConfig = await webpackConfig;
181+
} catch (error) {
182+
fecLogger(LogType.error, 'Failed to get the Webpack config:', error);
183+
process.exit(1);
184+
}
185+
186+
// Process environment variables, SPA fallback
187+
try {
188+
SPAFallback = fecConfig?.SPAFallback ?? true;
189+
await configureEnvVars(fecConfig, argv, cwd);
190+
} catch (error) {
191+
fecLogger(LogType.error, 'Failed to setup environment from args and config:', error);
192+
process.exit(1);
193+
}
194+
195+
// Setup Routes
196+
let cdnPath: string;
197+
let routesConfigPath: string;
198+
const staticPort = argv.staticPort ?? '8003';
199+
try {
200+
cdnPath = getCdnPath(fecConfig, webpackConfig, cwd);
201+
routesConfigPath = createRoutesConfig(fecConfig, cdnPath, staticPort, SPAFallback);
202+
} catch (error) {
203+
fecLogger(LogType.error, 'Failed to generate the proxy routes config:', error);
204+
process.exit(1);
205+
}
206+
207+
// Setup Container
208+
execBin = checkContainerRuntime();
209+
stopContainer(DEV_PROXY_CONTAINER_NAME);
210+
stopContainer(CHROME_CONTAINTER_NAME);
211+
pullImage(DEV_PROXY_CONTAINER_NAME, DEV_PROXY_IMAGE_REPO, LATEST_IMAGE_TAG);
212+
removeContainer(DEV_PROXY_CONTAINER_NAME);
213+
214+
// Exec
215+
let commands: Command[] = [];
216+
let waitOnProcess: ReturnType<typeof exec> | undefined = undefined;
217+
218+
const cleanup = () => {
219+
commands.forEach((cmd) => {
220+
if (cmd.pid) {
221+
treeKill(cmd.pid, 'SIGKILL');
222+
}
223+
});
224+
if (waitOnProcess?.pid) {
225+
treeKill(waitOnProcess.pid, 'SIGKILL');
226+
}
227+
stopContainer(DEV_PROXY_CONTAINER_NAME);
228+
if (SPAFallback) {
229+
stopContainer(CHROME_CONTAINTER_NAME);
230+
}
231+
console.log('\n');
232+
};
233+
234+
try {
235+
const outputPath = webpackConfig.output.path;
236+
const proxyEnvVar = process.env.HCC_ENV === 'stage' ? '-e HTTPS_PROXY=$RH_PROXY_URL' : '';
237+
const proxyVerbose = fecConfig?.proxyVerbose ? `&& ${execBin} logs -f ${DEV_PROXY_CONTAINER_NAME}` : '';
238+
const appUrl = fecConfig?.appUrl;
239+
240+
try {
241+
if (SPAFallback) {
242+
const handleServerError = (error: Error) => {
243+
fecLogger(LogType.error, error);
244+
cleanup();
245+
process.exit(1);
246+
};
247+
248+
await serveChrome(
249+
outputPath,
250+
process.env.FEC_CHROME_HOST ?? '127.0.0.1',
251+
handleServerError,
252+
process.env.CLOUDOT_ENV === 'prod',
253+
parseInt(process.env.FEC_CHROME_PORT!),
254+
).catch((error) => {
255+
fecLogger(LogType.error, 'Chrome server stopped!');
256+
handleServerError(error);
257+
});
258+
}
259+
} catch (error) {
260+
fecLogger(LogType.error, 'Unable to start local Chrome UI server!');
261+
fecLogger(LogType.error, error);
262+
process.exit(1);
263+
}
264+
265+
const { result, commands: cmds } = concurrently(
266+
[
267+
{
268+
command: `npm exec -- webpack --config ${webpackConfigPath} --watch --output-path ${path.join(outputPath, cdnPath)}`,
269+
name: 'BUILD',
270+
prefixColor: 'bgBlue',
271+
},
272+
{
273+
command: `npm exec -- http-server ${outputPath} -p ${staticPort} -c-1 -a :: --cors=*`,
274+
name: 'SERVE',
275+
prefixColor: 'bgGreen',
276+
},
277+
{
278+
command: `${execBin} run -d -e HCC_ENV=${process.env.HCC_ENV} -e HCC_ENV_URL=${process.env.HCC_ENV_URL} ${proxyEnvVar} -p ${argv.port || 1337}:${DEV_PROXY_CONTAINER_PORT} -v "${routesConfigPath}:/config/routes.json:ro,Z" --name ${DEV_PROXY_CONTAINER_NAME} ${DEV_PROXY_IMAGE_REPO}:${LATEST_IMAGE_TAG} ${proxyVerbose}`,
279+
name: 'PROXY',
280+
env: { RH_PROXY_URL: PROXY_URL },
281+
prefixColor: 'bgMagenta',
282+
},
283+
],
284+
{
285+
prefix: 'name',
286+
killOthers: ['failure'],
287+
pauseInputStreamOnFinish: true,
288+
},
289+
);
290+
commands = cmds;
291+
292+
waitOnProcess = exec(
293+
`npm exec -- wait-on --timeout 60000 --delay 10000 https://${process.env.HCC_ENV}.foo.redhat.com:${argv.port || 1337}${appUrl}`,
294+
(error) => {
295+
if (error) {
296+
return;
297+
}
298+
console.log('\u001b[43m[INFO ]\x1b[0m App should run on:');
299+
console.log(`\u001b[43m[INFO ]\x1b[0m \t- \u001b[34mhttps://${process.env.HCC_ENV}.foo.redhat.com:${argv.port || 1337}${appUrl}\x1b[0m`);
300+
console.log('\u001b[43m[INFO ]\x1b[0m Static assets are available at:');
301+
console.log(`\u001b[43m[INFO ]\x1b[0m \t- \u001b[34mhttps://${process.env.HCC_ENV}.foo.redhat.com:${argv.port || 1337}${cdnPath}\x1b[0m`);
302+
},
303+
);
304+
305+
result.then(
306+
() => {
307+
cleanup();
308+
process.exit(0);
309+
},
310+
() => {
311+
cleanup();
312+
process.exit(0);
313+
},
314+
);
315+
} catch (error) {
316+
fecLogger(LogType.error, error);
317+
process.exit(1);
318+
}
319+
}
320+
321+
module.exports = devProxyScript;

0 commit comments

Comments
 (0)