import * as plugins from './plugins.js'; import type { TDockerResourceData } from './interfaces/index.js'; export interface IDockerResponse { statusCode: number; body: unknown; } export const encodeDockerResourceData = ( dataArg: TDockerResourceData, ): string => { if (typeof dataArg === 'string') { return plugins.buffer.Buffer.from(dataArg, 'utf8').toString('base64'); } if (dataArg instanceof Uint8Array) { return plugins.buffer.Buffer.from( dataArg.buffer, dataArg.byteOffset, dataArg.byteLength, ).toString('base64'); } throw new TypeError('Docker resource data must be a string or Uint8Array'); }; export const formatDockerResponseBody = (bodyArg: unknown): string => { if (typeof bodyArg === 'string') { return bodyArg; } try { const serializedBody = JSON.stringify(bodyArg); return serializedBody === undefined ? String(bodyArg) : serializedBody; } catch { return String(bodyArg); } }; export const assertDockerResponseStatus = ( operationArg: string, expectedStatusArg: number, responseArg: IDockerResponse, ): void => { if (responseArg.statusCode !== expectedStatusArg) { throw new Error( `${operationArg} failed: expected status ${expectedStatusArg}, actual status ${responseArg.statusCode}; response body: ${formatDockerResponseBody(responseArg.body)}`, ); } }; export const assertDockerResponseStatusOneOf = ( operationArg: string, expectedStatusesArg: number[], responseArg: IDockerResponse, ): void => { if (!expectedStatusesArg.includes(responseArg.statusCode)) { throw new Error( `${operationArg} failed: expected status ${expectedStatusesArg.join(' or ')}, actual status ${responseArg.statusCode}; response body: ${formatDockerResponseBody(responseArg.body)}`, ); } }; export const buildDockerFilterRoute = ( routeArg: string, filtersArg?: Record, ): string => { if (!filtersArg || Object.keys(filtersArg).length === 0) { return routeArg; } const separator = routeArg.includes('?') ? '&' : '?'; return `${routeArg}${separator}filters=${encodeURIComponent(JSON.stringify(filtersArg))}`; }; export function assertNonemptyDockerString( valueArg: unknown, descriptionArg: string, ): asserts valueArg is string { if (typeof valueArg !== 'string' || valueArg.trim().length === 0) { throw new TypeError(`${descriptionArg} must be a nonempty string`); } } export function assertDockerContainerId( valueArg: unknown, descriptionArg = 'Docker container ID', ): asserts valueArg is string { assertNonemptyDockerString(valueArg, descriptionArg); if (!/^[0-9a-f]{64}$/.test(valueArg)) { throw new TypeError( `${descriptionArg} must be a complete immutable 64-character hexadecimal ID`, ); } } export function assertDockerNetworkId( valueArg: unknown, descriptionArg = 'Docker network ID', ): asserts valueArg is string { assertNonemptyDockerString(valueArg, descriptionArg); if (!/^(?:[0-9a-f]{64}|[0-9a-z]{25})$/.test(valueArg)) { throw new TypeError( `${descriptionArg} must be a complete immutable 64-character hexadecimal local ID or 25-character base-36 swarm ID`, ); } } export function assertDockerServiceId( valueArg: unknown, descriptionArg = 'Docker service ID', ): asserts valueArg is string { assertNonemptyDockerString(valueArg, descriptionArg); if (!/^[0-9a-z]{25}$/.test(valueArg)) { throw new TypeError( `${descriptionArg} must be a complete immutable 25-character base-36 swarm ID`, ); } } export function assertDockerNodeId( valueArg: unknown, descriptionArg = 'Docker node ID', ): asserts valueArg is string { assertNonemptyDockerString(valueArg, descriptionArg); if (!/^[0-9a-z]{25}$/.test(valueArg)) { throw new TypeError( `${descriptionArg} must be a complete immutable 25-character base-36 swarm ID`, ); } } export function assertDockerImageId( valueArg: unknown, descriptionArg = 'Docker image ID', ): asserts valueArg is string { assertNonemptyDockerString(valueArg, descriptionArg); if (!/^sha256:[0-9a-f]{64}$/.test(valueArg)) { throw new TypeError( `${descriptionArg} must be a complete immutable sha256 local image ID`, ); } } export function assertDockerMutableImageReference( valueArg: unknown, ): asserts valueArg is string { assertNonemptyDockerString(valueArg, 'Docker mutable image reference'); if (valueArg !== valueArg.trim() || /[\s\u0000-\u001f\u007f]/.test(valueArg)) { throw new TypeError( 'Docker mutable image reference must not contain whitespace or control characters', ); } if (valueArg.includes('@')) { throw new TypeError('Docker mutable image reference must use an explicit tag, not a digest'); } const finalComponent = valueArg.slice(valueArg.lastIndexOf('/') + 1); const tagSeparator = finalComponent.lastIndexOf(':'); const repositoryName = finalComponent.slice(0, tagSeparator); const tag = finalComponent.slice(tagSeparator + 1); if ( tagSeparator <= 0 || !repositoryName || repositoryName.includes(':') || !/^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$/.test(tag) ) { throw new TypeError('Docker mutable image reference must include a valid explicit tag'); } } export function assertNonRootDockerUser( valueArg: unknown, descriptionArg: string, ): asserts valueArg is string { assertNonemptyDockerString(valueArg, descriptionArg); const [userPart] = valueArg.split(':', 1); const normalizedUser = userPart.trim(); if ( normalizedUser.length === 0 || normalizedUser.toLowerCase() === 'root' || /^[+-]?0+$/.test(normalizedUser) ) { throw new TypeError( `${descriptionArg} must identify an explicit non-root user`, ); } } const formatUnknownError = (errorArg: unknown): string => errorArg instanceof Error ? errorArg.message : formatDockerResponseBody(errorArg); export const rollbackFailedDockerCreation = async ( resourceDescriptionArg: string, verificationErrorArg: unknown, rollbackArg: () => Promise, ): Promise => { try { await rollbackArg(); } catch (rollbackError) { throw new AggregateError( [verificationErrorArg, rollbackError], `${resourceDescriptionArg} post-create verification failed: ${formatUnknownError(verificationErrorArg)}; rollback removal failed: ${formatUnknownError(rollbackError)}`, ); } throw verificationErrorArg; }; export const assertNonnegativeInteger = ( valueArg: number, descriptionArg: string, ): void => { if (!Number.isSafeInteger(valueArg) || valueArg < 0) { throw new TypeError(`${descriptionArg} must be a nonnegative safe integer`); } };