@forge/cli-shared

8.15.2-next.0-experimental-1dcd5938.15.2-next.1
out/tunnel/docker-compose-lifecycle.js
~out/tunnel/docker-compose-lifecycle.jsModified
+23−212
Index: package/out/tunnel/docker-compose-lifecycle.js
===================================================================
--- package/out/tunnel/docker-compose-lifecycle.js
+++ package/out/tunnel/docker-compose-lifecycle.js
@@ -1,12 +1,11 @@
 "use strict";
 Object.defineProperty(exports, "__esModule", { value: true });
-exports.processDockerAuthentication = exports.stopDockerComposeStack = exports.determineComposeFlags = exports.startDockerComposeStack = exports.deleteDockerComposeFile = exports.generateContainersDockerComposeFile = exports.DockerAuthenticationError = exports.CannotUseBothImageAndBuildContextError = exports.MissingImageOrBuildContextError = exports.UnableToParseDockerComposeFileError = exports.DockerUnableToStartError = exports.DockerUnableToPullProxySidecarImage = exports.InvalidContainerServicePort = exports.STARTUP_TIMER_BUFFER_MS = exports.STARTUP_TIMER_MS = exports.K8S_AUTH_TOKEN_FILENAME = exports.CONTAINER_SERVICE_ASSETS = exports.PROXY_SIDECAR_VOLUME_DIR = void 0;
+exports.processDockerAuthentication = exports.stopDockerComposeStack = exports.startDockerComposeStack = exports.deleteDockerComposeFile = exports.generateContainersDockerComposeFile = exports.DockerAuthenticationError = exports.CannotUseBothImageAndBuildContextError = exports.MissingImageOrBuildContextError = exports.DockerUnableToStartError = exports.DockerUnableToPullProxySidecarImage = exports.InvalidContainerServicePort = exports.K8S_AUTH_TOKEN_FILENAME = exports.CONTAINER_SERVICE_ASSETS = exports.PROXY_SIDECAR_VOLUME_DIR = void 0;
 const tslib_1 = require("tslib");
 const fs = tslib_1.__importStar(require("fs"));
 const path = tslib_1.__importStar(require("path"));
 const yaml = tslib_1.__importStar(require("yaml"));
-const http = tslib_1.__importStar(require("http"));
 const docker_compose_1 = require("docker-compose");
 const shared_1 = require("../shared");
 const text_1 = require("../ui/text");
 const tunnel_options_1 = require("./tunnel-options");
@@ -14,16 +13,8 @@
 exports.PROXY_SIDECAR_VOLUME_DIR = '/forge/container';
 exports.CONTAINER_SERVICE_ASSETS = '.container-service-assets';
 exports.K8S_AUTH_TOKEN_FILENAME = 'local-account';
 const PROXY_SIDECAR_SERVICE_NAME = 'proxy-sidecar';
-const LIFECYCLE_PATTERNS = [
-    /Container .* (Starting|Started|Stopping|Stopped|Creating|Created|Recreated)/,
-    /Network .* (Creating|Created|Removing|Removed)/,
-    /Volume .* (Creating|Created|Removing|Removed)/,
-    /\[[\+\-]\] Running/
-];
-exports.STARTUP_TIMER_MS = 30 * 1000;
-exports.STARTUP_TIMER_BUFFER_MS = 5 * 1000;
 class InvalidContainerServicePort extends shared_1.UserError {
     constructor(serviceKey) {
         super(text_1.Text.error.invalidServicePort(serviceKey));
     }
@@ -40,14 +31,8 @@
         super(text_1.Text.tunnel.unableToStartDockerComposeStack(err?.message ?? 'Unknown Error Occurred.'));
     }
 }
 exports.DockerUnableToStartError = DockerUnableToStartError;
-class UnableToParseDockerComposeFileError extends shared_1.UserError {
-    constructor(serviceKey, err) {
-        super(text_1.Text.tunnel.UnableToParseDockerComposeFileError(serviceKey, err?.message ?? 'Unknown Error Occurred.'));
-    }
-}
-exports.UnableToParseDockerComposeFileError = UnableToParseDockerComposeFileError;
 class MissingImageOrBuildContextError extends shared_1.UserError {
     constructor(containerKey) {
         super(text_1.Text.tunnel.missingImageOrBuildContext(containerKey));
     }
@@ -66,9 +51,9 @@
 }
 exports.DockerAuthenticationError = DockerAuthenticationError;
 const generateContainersDockerComposeFile = async (services, appId, envId) => {
     const filesGenerated = {};
-    for (const [serviceIndex, service] of services.entries()) {
+    for (const service of services) {
         const { key: serviceKey, containers } = service;
         const containersWithTunnelConfig = containers.filter((container) => !!container.tunnel);
         if (containersWithTunnelConfig.length > 0) {
             const port = await (0, tunnel_options_1.getServicePort)(services, serviceKey);
@@ -86,31 +71,25 @@
                     container_name: container.key,
                     ...container.tunnel?.docker,
                     depends_on: [PROXY_SIDECAR_SERVICE_NAME]
                 };
-                if (config.build?.context) {
-                    config.build.context = adjustFilePath(config.build.context);
+                if (config.build?.context && !path.isAbsolute(config.build.context)) {
+                    let context = config.build.context;
+                    if (context.startsWith('./')) {
+                        context = context.slice(2);
+                    }
+                    config.build.context = '../' + context;
                 }
-                if (config.volumes) {
-                    config.volumes = config.volumes.map((volume) => adjustFilePath(volume));
-                }
-                if (config.develop?.watch) {
-                    config.develop.watch = config.develop.watch.map((watchConfig) => ({
-                        ...watchConfig,
-                        path: adjustFilePath(watchConfig.path)
-                    }));
-                }
                 const envArray = container?.tunnel?.docker.environment ?? [];
                 const filteredEnvArray = envArray.filter((envVar) => !envVar.startsWith('FORGE_EGRESS_PROXY_URL='));
                 filteredEnvArray.push('FORGE_EGRESS_PROXY_URL=http://proxy-sidecar:7072');
                 config.environment = filteredEnvArray;
                 return [container.key, config];
             }));
-            const healthEndpointPath = containersWithTunnelConfig[0].health.route.path;
             const dockerComposeConfig = {
                 services: {
                     ...containerConfig,
-                    ...(await getProxySidecarConfig(serviceKey, Object.keys(containerConfig), port, appId, envId, serviceIndex, healthEndpointPath))
+                    ...(await getProxySidecarConfig(serviceKey, Object.keys(containerConfig), port, appId, envId))
                 }
             };
             const yamlString = yaml.stringify(dockerComposeConfig);
             const filePath = getContainerDockerComposePath(serviceKey);
@@ -120,19 +99,9 @@
     }
     return filesGenerated;
 };
 exports.generateContainersDockerComposeFile = generateContainersDockerComposeFile;
-const adjustFilePath = (filePath) => {
-    if (path.isAbsolute(filePath)) {
-        return filePath;
-    }
-    let adjusted = filePath;
-    if (adjusted.startsWith('./')) {
-        adjusted = adjusted.slice(2);
-    }
-    return path.join('..', adjusted);
-};
-const getProxySidecarConfig = async (serviceKey, containerKeys, port, appId, envId, serviceIndex, healthEndpointPath) => {
+const getProxySidecarConfig = async (serviceKey, containerKeys, port, appId, envId) => {
     let fopBaseUrl = 'https://forge-outbound-proxy.services.atlassian.com';
     let jwksUrl = 'https://forge.cdn.prod.atlassian-dev.net/.well-known/jwks.json';
     let proxySidecarImage = 'forge-ecr.services.atlassian.com/forge-platform/proxy-sidecar:latest';
     if (process.env.FORGE_GRAPHQL_GATEWAY?.startsWith('https://api-private.stg.atlassian.com/graphql')) {
@@ -154,16 +123,12 @@
                 `APP_ID=ari:cloud:ecosystem::app/${appIdShort}`,
                 `ENV_ID=ari:cloud:ecosystem::environment/${appIdShort}/${envId}`,
                 `JWKS_URL=${jwksUrl}`,
                 `IS_LOCAL_DEV=true`,
-                `K8S_AUTH_TOKEN_PATH=${exports.PROXY_SIDECAR_VOLUME_DIR}/${exports.K8S_AUTH_TOKEN_FILENAME}`,
-                `${containerKeys[0]}_CONTAINER_HEALTHCHECK=http://${containerKeys[0]}:8080${healthEndpointPath}`
+                `K8S_AUTH_TOKEN_PATH=${exports.PROXY_SIDECAR_VOLUME_DIR}/${exports.K8S_AUTH_TOKEN_FILENAME}`
             ],
             volumes: [`../${exports.CONTAINER_SERVICE_ASSETS}:${exports.PROXY_SIDECAR_VOLUME_DIR}:ro`],
-            ports: [
-                `${port}:${tunnel_options_1.DEFAULT_PROXY_INGRESS_PORT}`,
-                `${tunnel_options_1.DEFAULT_PROXY_HEALTHCHECK_PORT_HOST_MACHINE + serviceIndex}:${tunnel_options_1.DEFAULT_PROXY_HEALTHCHECK_PORT}`
-            ]
+            ports: [`${port}:${tunnel_options_1.DEFAULT_PROXY_INGRESS_PORT}`]
         }
     };
 };
 const getProxySidecarContainerName = (serviceKey) => {
@@ -188,9 +153,9 @@
         }
     }
 };
 exports.deleteDockerComposeFile = deleteDockerComposeFile;
-const startDockerComposeStack = async (dockerComposeFilePath, serviceKey, logger) => {
+const startDockerComposeStack = async (dockerComposeFilePath, serviceKey) => {
     try {
         await (0, docker_compose_1.pullOne)('proxy-sidecar', {
             cwd: path.dirname(dockerComposeFilePath),
             log: true,
@@ -199,156 +164,23 @@
     }
     catch (err) {
         throw new DockerUnableToPullProxySidecarImage(err);
     }
-    await waitForContainersToStart(dockerComposeFilePath, serviceKey, logger);
-};
-exports.startDockerComposeStack = startDockerComposeStack;
-const determineComposeFlags = async (dockerComposeFilePath, serviceKey, logger) => {
-    const flags = ['--build', '--quiet-pull'];
     try {
-        const composeConfig = getComposeConfig(dockerComposeFilePath);
-        const hasWatchConfig = Object.values(composeConfig?.services ?? {}).some((service) => service?.develop?.watch);
-        if (hasWatchConfig) {
-            logger.info(`Hot reload config detected. Starting up ${serviceKey} containers using the --watch flag.`);
-            flags.push('--watch');
-        }
-    }
-    catch (_) { }
-    return flags;
-};
-exports.determineComposeFlags = determineComposeFlags;
-const getComposeConfig = (dockerComposeFilePath) => {
-    try {
-        const composeContent = fs.readFileSync(dockerComposeFilePath, 'utf8');
-        const composeConfig = yaml.parse(composeContent);
-        return composeConfig;
-    }
-    catch (_) { }
-    return undefined;
-};
-const extractProxySidecarHealthcheckUrl = (composeConfig) => {
-    if (!composeConfig?.services) {
-        return undefined;
-    }
-    let healthCheckHostPort = undefined;
-    try {
-        const proxySidecarService = composeConfig.services[PROXY_SIDECAR_SERVICE_NAME];
-        if (proxySidecarService && proxySidecarService.ports && proxySidecarService.ports.length > 0) {
-            const portMapping = proxySidecarService.ports.find((portMapping) => {
-                const parts = portMapping.split(':');
-                return parts[1] === tunnel_options_1.DEFAULT_PROXY_HEALTHCHECK_PORT.toString();
-            });
-            if (portMapping) {
-                healthCheckHostPort = portMapping.split(':')[0];
-            }
-        }
-    }
-    catch (_) { }
-    if (healthCheckHostPort) {
-        return `http://localhost:${healthCheckHostPort}/health`;
-    }
-    return undefined;
-};
-const checkContainerHealth = async (url) => {
-    return new Promise((resolve) => {
-        const request = http.get(url, { timeout: 3000 }, (res) => {
-            resolve((res.statusCode ?? 0) >= 200 && (res.statusCode ?? 0) < 300);
-        });
-        request.on('error', () => {
-            resolve(false);
-        });
-        request.on('timeout', () => {
-            request.destroy();
-            resolve(false);
-        });
-    });
-};
-const pollContainerHealth = async (proxySidecarHealthEndpointUrl, logger) => {
-    const maxAttempts = exports.STARTUP_TIMER_MS / 1000 - exports.STARTUP_TIMER_BUFFER_MS / 1000;
-    const pollInterval = 1000;
-    for (let attempt = 1; attempt <= maxAttempts; attempt++) {
-        const isHealthy = await checkContainerHealth(proxySidecarHealthEndpointUrl);
-        if (isHealthy) {
-            logger.info('All health endpoints responded successfully!');
-            return true;
-        }
-        if (attempt === maxAttempts) {
-            logger.warn(`Containers did not become healthy within ${exports.STARTUP_TIMER_MS / 1000}s`);
-            return false;
-        }
-        await new Promise((resolve) => setTimeout(resolve, pollInterval));
-    }
-    return false;
-};
-const waitForContainersToStart = async (dockerComposeFilePath, serviceKey, logger) => {
-    const composeFlags = await (0, exports.determineComposeFlags)(dockerComposeFilePath, serviceKey, logger);
-    let proxySidecarHealthEndpointUrl;
-    try {
-        const composeConfig = getComposeConfig(dockerComposeFilePath);
-        proxySidecarHealthEndpointUrl = extractProxySidecarHealthcheckUrl(composeConfig);
-    }
-    catch (err) {
-        throw new UnableToParseDockerComposeFileError(serviceKey, err);
-    }
-    return new Promise((resolve, reject) => {
-        let containersStarted = false;
-        const startupTimer = setTimeout(() => {
-            if (!containersStarted) {
-                containersStarted = true;
-                logger.info(`Startup timeout reached (${exports.STARTUP_TIMER_MS / 1000}s). Starting up the forge tunnel...`);
-                resolve();
-            }
-        }, exports.STARTUP_TIMER_MS);
-        const logFilter = (chunk) => {
-            const output = chunk.toString();
-            const lines = output.split('\n');
-            lines.forEach((line) => {
-                if (!line.trim())
-                    return;
-                if (LIFECYCLE_PATTERNS.some((pattern) => pattern.test(line))) {
-                    logger.info(line);
-                }
-            });
-        };
-        (0, docker_compose_1.execCompose)('up', composeFlags, {
+        await (0, docker_compose_1.upAll)({
             cwd: path.dirname(dockerComposeFilePath),
-            log: false,
+            log: true,
             config: dockerComposeFilePath,
             composeOptions: [`-p${serviceKey}`],
-            callback: logFilter
-        }).catch((error) => {
-            clearTimeout(startupTimer);
-            const errorMessage = error?.err || error?.out || error?.message || 'Unknown Error Occurred.';
-            reject(new DockerUnableToStartError(new Error(errorMessage)));
+            commandOptions: ['--build']
         });
-        setTimeout(async () => {
-            if (containersStarted)
-                return;
-            try {
-                if (proxySidecarHealthEndpointUrl) {
-                    logger.info(`Polling health endpoints for service: ${serviceKey}`);
-                    const allHealthy = await pollContainerHealth(proxySidecarHealthEndpointUrl, logger);
-                    if (allHealthy && !containersStarted) {
-                        containersStarted = true;
-                        clearTimeout(startupTimer);
-                        resolve();
-                    }
-                }
-                else {
-                    logger.info('No health check endpoints found. Continuing with tunnel startup...');
-                    containersStarted = true;
-                    clearTimeout(startupTimer);
-                    resolve();
-                }
-            }
-            catch (err) {
-                logger.warn(`Health check polling failed: ${err.message}. Relying on startup timeout...`);
-            }
-        }, exports.STARTUP_TIMER_BUFFER_MS);
-    });
+    }
+    catch (err) {
+        throw new DockerUnableToStartError(err);
+    }
 };
-const stopDockerComposeStack = async (configFile, logger, composeFiles) => {
+exports.startDockerComposeStack = startDockerComposeStack;
+const stopDockerComposeStack = async (configFile, composeFiles) => {
     if (!composeFiles || Object.keys(composeFiles).length === 0)
         return;
     const { services } = await configFile.readConfig();
     const serviceWithTunnelConfigExists = services?.some((service) => service.containers?.some((container) => {
@@ -357,15 +189,9 @@
     if (!services || services.length === 0 || !serviceWithTunnelConfigExists)
         return;
     await Promise.all(Object.entries(composeFiles).map(async ([serviceKey, file]) => {
         try {
-            await (0, docker_compose_1.downAll)({
-                cwd: '.',
-                log: false,
-                config: file,
-                composeOptions: [`-p${serviceKey}`],
-                callback: createCustomLogFilter(logger)
-            });
+            await (0, docker_compose_1.downAll)({ cwd: '.', log: true, config: file, composeOptions: [`-p${serviceKey}`] });
             await (0, exports.deleteDockerComposeFile)(file);
         }
         catch (err) {
             throw new Error(text_1.Text.tunnel.unableToStopDockerComposeStack(serviceKey, err.message ?? 'Unknown Error Occurred.'));
@@ -373,23 +199,8 @@
     }));
     deleteContainerServiceAssetsDir();
 };
 exports.stopDockerComposeStack = stopDockerComposeStack;
-const createCustomLogFilter = (logger) => {
-    let buffer = '';
-    return (chunk) => {
-        buffer += chunk.toString();
-        const lines = buffer.split('\n');
-        buffer = lines.pop() || '';
-        lines.forEach((line) => {
-            if (!line.trim())
-                return;
-            if (LIFECYCLE_PATTERNS.some((pattern) => pattern.test(line))) {
-                logger.info(line);
-            }
-        });
-    };
-};
 const processDockerAuthentication = async (childProcess) => {
     await new Promise((resolve, reject) => {
         childProcess.on('close', (code) => {
             if (code === 0) {