diff --git a/.github/workflows/test-blacksmith.yml b/.github/workflows/test-blacksmith.yml index 2297180..0fd7e60 100644 --- a/.github/workflows/test-blacksmith.yml +++ b/.github/workflows/test-blacksmith.yml @@ -13,6 +13,9 @@ jobs: - name: Checkout action repo uses: actions/checkout@v4 + - name: Assert BLACKSMITH_AGENT_ADDR is advertised + run: test -n "$BLACKSMITH_AGENT_ADDR" + - name: Test checkout with git mirror (self) uses: ./ with: diff --git a/__test__/blacksmith-cache.test.ts b/__test__/blacksmith-cache.test.ts index 144409e..2094ddc 100644 --- a/__test__/blacksmith-cache.test.ts +++ b/__test__/blacksmith-cache.test.ts @@ -147,6 +147,8 @@ describe('blacksmith-cache tests', () => { beforeEach(() => { jest.resetModules() process.env = {...originalEnv} + process.env['BLACKSMITH_AGENT_ADDR'] = '192.168.127.1' + process.env['BLACKSMITH_STICKY_DISK_GRPC_PORT'] = '5557' mockIsRunningInContainer.mockReturnValue(false) }) @@ -166,6 +168,18 @@ describe('blacksmith-cache tests', () => { expect(blacksmithCache.shouldUseBlacksmithCache()).toBe(false) }) + it('returns false when BLACKSMITH_AGENT_ADDR is not set', () => { + process.env['BLACKSMITH_VM_ID'] = 'test-vm-id' + delete process.env['BLACKSMITH_AGENT_ADDR'] + expect(blacksmithCache.shouldUseBlacksmithCache()).toBe(false) + }) + + it('returns false when BLACKSMITH_STICKY_DISK_GRPC_PORT is not set', () => { + process.env['BLACKSMITH_VM_ID'] = 'test-vm-id' + delete process.env['BLACKSMITH_STICKY_DISK_GRPC_PORT'] + expect(blacksmithCache.shouldUseBlacksmithCache()).toBe(false) + }) + it('returns false when BLACKSMITH_BYPASS_CHECKOUT=true (control-plane kill switch)', () => { process.env['BLACKSMITH_VM_ID'] = 'test-vm-id' process.env['BLACKSMITH_BYPASS_CHECKOUT'] = 'true' diff --git a/dist/index.js b/dist/index.js index 0243e17..ce4a63e 100644 --- a/dist/index.js +++ b/dist/index.js @@ -41,6 +41,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge Object.defineProperty(exports, "__esModule", ({ value: true })); exports.getMountPoint = getMountPoint; exports.isBlacksmithEnvironment = isBlacksmithEnvironment; +exports.getAgentAddr = getAgentAddr; +exports.getGrpcPort = getGrpcPort; exports.isAllowedInsideContainer = isAllowedInsideContainer; exports.shouldUseBlacksmithCache = shouldUseBlacksmithCache; exports.getMirrorPath = getMirrorPath; @@ -59,7 +61,9 @@ const connect_node_1 = __nccwpck_require__(1125); const stickydisk_connect_1 = __nccwpck_require__(2880); const retryHelper = __importStar(__nccwpck_require__(2155)); const container_detector_1 = __nccwpck_require__(6424); -const GRPC_PORT = process.env.BLACKSMITH_STICKY_DISK_GRPC_PORT || '5557'; +// Without a deadline, a black-holed dial stalls the checkout until the OS +// gives up on the TCP handshake. +const AGENT_RPC_TIMEOUT_MS = 45000; const MOUNT_BASE = '/blacksmith-git-mirror'; const MIRROR_VERSION = 'v1'; const REFRESH_TIMEOUT_SECS = 120; // 2 minutes @@ -86,6 +90,12 @@ function getMountPoint(owner, repo) { function isBlacksmithEnvironment() { return !!process.env.BLACKSMITH_VM_ID; } +function getAgentAddr() { + return process.env.BLACKSMITH_AGENT_ADDR || undefined; +} +function getGrpcPort() { + return process.env.BLACKSMITH_STICKY_DISK_GRPC_PORT || undefined; +} /** * Escape hatch for container jobs that deliberately pass the runner's * block devices through to the container (e.g. `options: @@ -113,6 +123,10 @@ function shouldUseBlacksmithCache() { if (!isBlacksmithEnvironment()) { return false; } + if (!getAgentAddr() || !getGrpcPort()) { + core.info('[blacksmith] BLACKSMITH_AGENT_ADDR or BLACKSMITH_STICKY_DISK_GRPC_PORT is not set; the Blacksmith agent is not reachable from this runner, falling back to actions/checkout behavior'); + return false; + } if (process.env.BLACKSMITH_BYPASS_CHECKOUT === 'true') { core.info('[blacksmith] BLACKSMITH_BYPASS_CHECKOUT=true — skipping Blacksmith git mirror cache and falling back to actions/checkout behavior'); return false; @@ -145,9 +159,14 @@ function getMirrorPath(owner, repo) { * Create a gRPC client for communicating with the Blacksmith VM agent */ function createBlacksmithClient() { - core.debug(`Creating Blacksmith agent client with port: ${GRPC_PORT}`); + const addr = getAgentAddr(); + const grpcPort = getGrpcPort(); + if (!addr || !grpcPort) { + throw new Error('BLACKSMITH_AGENT_ADDR or BLACKSMITH_STICKY_DISK_GRPC_PORT is not set; cannot dial the Blacksmith agent'); + } + core.debug(`Creating Blacksmith agent client for ${addr}:${grpcPort}`); const transport = (0, connect_node_1.createGrpcTransport)({ - baseUrl: `http://192.168.127.1:${GRPC_PORT}`, + baseUrl: `http://${addr}:${grpcPort}`, httpVersion: '2' }); return (0, connect_1.createClient)(stickydisk_connect_1.StickyDiskService, transport); @@ -219,14 +238,18 @@ function setupCache(owner, repo) { return __awaiter(this, void 0, void 0, function* () { const client = createBlacksmithClient(); const stickyDiskKey = `${owner}-${repo}`; - // Test connection + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), AGENT_RPC_TIMEOUT_MS); + // Rethrow the original error so callers can classify it from the gRPC code. core.info(`[git-mirror] Connecting to Blacksmith agent for ${stickyDiskKey}`); try { - yield client.up({}); + yield client.up({}, { signal: controller.signal }); core.debug('[git-mirror] Successfully connected to Blacksmith agent'); } catch (error) { - throw new Error(`gRPC connection test failed: ${error.message}`); + clearTimeout(timeoutId); + core.warning(`[git-mirror] gRPC connection test failed: ${error.message}`); + throw error; } core.info(`[git-mirror] Requesting sticky disk for ${stickyDiskKey}`); // Request sticky disk from VM agent @@ -243,7 +266,7 @@ function setupCache(owner, repo) { vmId: process.env.BLACKSMITH_VM_ID || '', repoName: repoName, stickyDiskToken: process.env.BLACKSMITH_STICKYDISK_TOKEN || '' - }); + }, { signal: controller.signal }); } catch (error) { // Check if this is a gRPC Aborted error indicating hydration in progress @@ -266,6 +289,9 @@ function setupCache(owner, repo) { // Re-throw other errors throw error; } + finally { + clearTimeout(timeoutId); + } const exposeId = response.exposeId || ''; const device = response.diskIdentifier || ''; if (!device) { @@ -3034,7 +3060,7 @@ const core = __importStar(__nccwpck_require__(2186)); const http = __importStar(__nccwpck_require__(3685)); const METRICS_PORT = process.env.BLACKSMITH_METRICS_HTTP_PORT || ''; const VM_ID = process.env.BLACKSMITH_VM_ID || ''; -const AGENT_IP = '192.168.127.1'; +const AGENT_IP = process.env.BLACKSMITH_AGENT_ADDR || ''; /** * Report an internal metric to the Blacksmith agent. * Fire-and-forget: errors are logged but never thrown. @@ -3045,6 +3071,10 @@ function reportInternalMetric(metricType, value, attributes) { core.debug('[metrics] BLACKSMITH_METRICS_HTTP_PORT not set, skipping metric'); return; } + if (!AGENT_IP) { + core.debug('[metrics] BLACKSMITH_AGENT_ADDR not set, skipping metric'); + return; + } const payload = JSON.stringify({ metric_type: metricType, value, diff --git a/src/blacksmith-cache.ts b/src/blacksmith-cache.ts index 2819bda..275ea08 100644 --- a/src/blacksmith-cache.ts +++ b/src/blacksmith-cache.ts @@ -8,7 +8,9 @@ import {StickyDiskService} from '@buf/blacksmith_vm-agent.connectrpc_es/stickydi import * as retryHelper from './retry-helper' import {isRunningInContainer} from './container-detector' -const GRPC_PORT = process.env.BLACKSMITH_STICKY_DISK_GRPC_PORT || '5557' +// Without a deadline, a black-holed dial stalls the checkout until the OS +// gives up on the TCP handshake. +const AGENT_RPC_TIMEOUT_MS = 45000 const MOUNT_BASE = '/blacksmith-git-mirror' const MIRROR_VERSION = 'v1' @@ -72,6 +74,14 @@ export function isBlacksmithEnvironment(): boolean { return !!process.env.BLACKSMITH_VM_ID } +export function getAgentAddr(): string | undefined { + return process.env.BLACKSMITH_AGENT_ADDR || undefined +} + +export function getGrpcPort(): string | undefined { + return process.env.BLACKSMITH_STICKY_DISK_GRPC_PORT || undefined +} + /** * Escape hatch for container jobs that deliberately pass the runner's * block devices through to the container (e.g. `options: @@ -102,6 +112,12 @@ export function shouldUseBlacksmithCache(): boolean { if (!isBlacksmithEnvironment()) { return false } + if (!getAgentAddr() || !getGrpcPort()) { + core.info( + '[blacksmith] BLACKSMITH_AGENT_ADDR or BLACKSMITH_STICKY_DISK_GRPC_PORT is not set; the Blacksmith agent is not reachable from this runner, falling back to actions/checkout behavior' + ) + return false + } if (process.env.BLACKSMITH_BYPASS_CHECKOUT === 'true') { core.info( '[blacksmith] BLACKSMITH_BYPASS_CHECKOUT=true — skipping Blacksmith git mirror cache and falling back to actions/checkout behavior' @@ -141,9 +157,16 @@ export function getMirrorPath(owner: string, repo: string): string { * Create a gRPC client for communicating with the Blacksmith VM agent */ function createBlacksmithClient() { - core.debug(`Creating Blacksmith agent client with port: ${GRPC_PORT}`) + const addr = getAgentAddr() + const grpcPort = getGrpcPort() + if (!addr || !grpcPort) { + throw new Error( + 'BLACKSMITH_AGENT_ADDR or BLACKSMITH_STICKY_DISK_GRPC_PORT is not set; cannot dial the Blacksmith agent' + ) + } + core.debug(`Creating Blacksmith agent client for ${addr}:${grpcPort}`) const transport = createGrpcTransport({ - baseUrl: `http://192.168.127.1:${GRPC_PORT}`, + baseUrl: `http://${addr}:${grpcPort}`, httpVersion: '2' }) @@ -228,13 +251,20 @@ export async function setupCache( const client = createBlacksmithClient() const stickyDiskKey = `${owner}-${repo}` - // Test connection + const controller = new AbortController() + const timeoutId = setTimeout(() => controller.abort(), AGENT_RPC_TIMEOUT_MS) + + // Rethrow the original error so callers can classify it from the gRPC code. core.info(`[git-mirror] Connecting to Blacksmith agent for ${stickyDiskKey}`) try { - await client.up({}) + await client.up({}, {signal: controller.signal}) core.debug('[git-mirror] Successfully connected to Blacksmith agent') } catch (error) { - throw new Error(`gRPC connection test failed: ${(error as Error).message}`) + clearTimeout(timeoutId) + core.warning( + `[git-mirror] gRPC connection test failed: ${(error as Error).message}` + ) + throw error } core.info(`[git-mirror] Requesting sticky disk for ${stickyDiskKey}`) @@ -245,15 +275,18 @@ export async function setupCache( const repoName = `${owner}/${repo}` let response try { - response = await client.getStickyDisk({ - stickyDiskKey: stickyDiskKey, - stickyDiskType: 'git_mirror', - region: process.env.BLACKSMITH_REGION || '', - installationModelId: process.env.BLACKSMITH_INSTALLATION_MODEL_ID || '', - vmId: process.env.BLACKSMITH_VM_ID || '', - repoName: repoName, - stickyDiskToken: process.env.BLACKSMITH_STICKYDISK_TOKEN || '' - }) + response = await client.getStickyDisk( + { + stickyDiskKey: stickyDiskKey, + stickyDiskType: 'git_mirror', + region: process.env.BLACKSMITH_REGION || '', + installationModelId: process.env.BLACKSMITH_INSTALLATION_MODEL_ID || '', + vmId: process.env.BLACKSMITH_VM_ID || '', + repoName: repoName, + stickyDiskToken: process.env.BLACKSMITH_STICKYDISK_TOKEN || '' + }, + {signal: controller.signal} + ) } catch (error) { // Check if this is a gRPC Aborted error indicating hydration in progress if (error instanceof ConnectError && error.code === Code.Aborted) { @@ -279,6 +312,8 @@ export async function setupCache( } // Re-throw other errors throw error + } finally { + clearTimeout(timeoutId) } const exposeId = (response as {exposeId?: string}).exposeId || '' diff --git a/src/internal-metrics.ts b/src/internal-metrics.ts index 519c481..8336149 100644 --- a/src/internal-metrics.ts +++ b/src/internal-metrics.ts @@ -3,7 +3,7 @@ import * as http from 'http' const METRICS_PORT = process.env.BLACKSMITH_METRICS_HTTP_PORT || '' const VM_ID = process.env.BLACKSMITH_VM_ID || '' -const AGENT_IP = '192.168.127.1' +const AGENT_IP = process.env.BLACKSMITH_AGENT_ADDR || '' /** * Report an internal metric to the Blacksmith agent. @@ -20,6 +20,10 @@ export async function reportInternalMetric( ) return } + if (!AGENT_IP) { + core.debug('[metrics] BLACKSMITH_AGENT_ADDR not set, skipping metric') + return + } const payload = JSON.stringify({ metric_type: metricType,