@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) {