diff --git a/changelog.md b/changelog.md index 620f1b5..c1a731c 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,14 @@ # Changelog +## 2026-02-07 - 1.13.0 - feat(docker) +add Docker context detection, rootless support, and context-aware buildx registry handling + +- Introduce DockerContext class to detect current Docker context and rootless mode and to log warnings and context info +- Add IDockerContextInfo interface and a new context option on build/config to pass explicit Docker context +- Propagate --context CLI flag into TsDockerManager.prepare so CLI commands can set an explicit Docker context +- Make buildx builder name context-aware (tsdocker-builder-) and log builder name/platforms +- Pass isRootless into local registry startup and build pipeline; emit rootless-specific warnings and registry reachability hint + ## 2026-02-06 - 1.12.0 - feat(docker) add detailed logging for buildx, build commands, local registry, and local dependency info diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index eafe2e7..0299981 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@git.zone/tsdocker', - version: '1.12.0', + version: '1.13.0', description: 'develop npm modules cross platform with docker' } diff --git a/ts/classes.dockercontext.ts b/ts/classes.dockercontext.ts new file mode 100644 index 0000000..1a8b1a7 --- /dev/null +++ b/ts/classes.dockercontext.ts @@ -0,0 +1,69 @@ +import * as plugins from './tsdocker.plugins.js'; +import { logger } from './tsdocker.logging.js'; +import type { IDockerContextInfo } from './interfaces/index.js'; + +const smartshellInstance = new plugins.smartshell.Smartshell({ executor: 'bash' }); + +export class DockerContext { + public contextInfo: IDockerContextInfo | null = null; + + /** Sets DOCKER_CONTEXT env var for explicit context selection. */ + public setContext(contextName: string): void { + process.env.DOCKER_CONTEXT = contextName; + logger.log('info', `Docker context explicitly set to: ${contextName}`); + } + + /** Detects current Docker context via `docker context inspect` and rootless via `docker info`. */ + public async detect(): Promise { + let name = 'default'; + let endpoint = 'unknown'; + + const contextResult = await smartshellInstance.execSilent( + `docker context inspect --format '{{json .}}'` + ); + if (contextResult.exitCode === 0 && contextResult.stdout) { + try { + const parsed = JSON.parse(contextResult.stdout.trim()); + const data = Array.isArray(parsed) ? parsed[0] : parsed; + name = data.Name || 'default'; + endpoint = data.Endpoints?.docker?.Host || 'unknown'; + } catch { /* fallback to defaults */ } + } + + let isRootless = false; + const infoResult = await smartshellInstance.execSilent( + `docker info --format '{{json .SecurityOptions}}'` + ); + if (infoResult.exitCode === 0 && infoResult.stdout) { + isRootless = infoResult.stdout.includes('name=rootless'); + } + + this.contextInfo = { name, endpoint, isRootless, dockerHost: process.env.DOCKER_HOST }; + return this.contextInfo; + } + + /** Logs context info prominently. */ + public logContextInfo(): void { + if (!this.contextInfo) return; + const { name, endpoint, isRootless, dockerHost } = this.contextInfo; + logger.log('info', '=== DOCKER CONTEXT ==='); + logger.log('info', `Context: ${name}`); + logger.log('info', `Endpoint: ${endpoint}`); + if (dockerHost) logger.log('info', `DOCKER_HOST: ${dockerHost}`); + logger.log('info', `Rootless: ${isRootless ? 'yes' : 'no'}`); + } + + /** Emits rootless-specific warnings. */ + public logRootlessWarnings(): void { + if (!this.contextInfo?.isRootless) return; + logger.log('warn', '[rootless] network=host in buildx is namespaced by rootlesskit'); + logger.log('warn', '[rootless] Local registry may have localhost vs 127.0.0.1 resolution quirks'); + } + + /** Returns context-aware builder name: tsdocker-builder- */ + public getBuilderName(): string { + const contextName = this.contextInfo?.name || 'default'; + const sanitized = contextName.replace(/[^a-zA-Z0-9_-]/g, '-'); + return `tsdocker-builder-${sanitized}`; + } +} diff --git a/ts/classes.dockerfile.ts b/ts/classes.dockerfile.ts index 41a718f..7d3f137 100644 --- a/ts/classes.dockerfile.ts +++ b/ts/classes.dockerfile.ts @@ -151,7 +151,7 @@ export class Dockerfile { } /** Starts a temporary registry:2 container on port 5234. */ - public static async startLocalRegistry(): Promise { + public static async startLocalRegistry(isRootless?: boolean): Promise { await smartshellInstance.execSilent( `docker rm -f ${LOCAL_REGISTRY_CONTAINER} 2>/dev/null || true` ); @@ -164,6 +164,9 @@ export class Dockerfile { // registry:2 starts near-instantly; brief wait for readiness await new Promise(resolve => setTimeout(resolve, 1000)); logger.log('info', `Started local registry at ${LOCAL_REGISTRY_HOST} (buildx dependency bridge)`); + if (isRootless) { + logger.log('warn', `[rootless] Registry on port ${LOCAL_REGISTRY_PORT} — if buildx cannot reach localhost:${LOCAL_REGISTRY_PORT}, try 127.0.0.1:${LOCAL_REGISTRY_PORT}`); + } } /** Stops and removes the temporary local registry container. */ @@ -191,14 +194,14 @@ export class Dockerfile { */ public static async buildDockerfiles( sortedArrayArg: Dockerfile[], - options?: { platform?: string; timeout?: number; noCache?: boolean; verbose?: boolean }, + options?: { platform?: string; timeout?: number; noCache?: boolean; verbose?: boolean; isRootless?: boolean }, ): Promise { const total = sortedArrayArg.length; const overallStart = Date.now(); const useRegistry = Dockerfile.needsLocalRegistry(sortedArrayArg, options); if (useRegistry) { - await Dockerfile.startLocalRegistry(); + await Dockerfile.startLocalRegistry(options?.isRootless); } try { diff --git a/ts/classes.tsdockermanager.ts b/ts/classes.tsdockermanager.ts index 47515b5..4a74970 100644 --- a/ts/classes.tsdockermanager.ts +++ b/ts/classes.tsdockermanager.ts @@ -5,6 +5,7 @@ import { Dockerfile } from './classes.dockerfile.js'; import { DockerRegistry } from './classes.dockerregistry.js'; import { RegistryStorage } from './classes.registrystorage.js'; import { TsDockerCache } from './classes.tsdockercache.js'; +import { DockerContext } from './classes.dockercontext.js'; import type { ITsDockerConfig, IBuildCommandOptions } from './interfaces/index.js'; const smartshellInstance = new plugins.smartshell.Smartshell({ @@ -18,17 +19,27 @@ export class TsDockerManager { public registryStorage: RegistryStorage; public config: ITsDockerConfig; public projectInfo: any; + public dockerContext: DockerContext; private dockerfiles: Dockerfile[] = []; constructor(config: ITsDockerConfig) { this.config = config; this.registryStorage = new RegistryStorage(); + this.dockerContext = new DockerContext(); } /** * Prepares the manager by loading project info and registries */ - public async prepare(): Promise { + public async prepare(contextArg?: string): Promise { + // Detect Docker context + if (contextArg) { + this.dockerContext.setContext(contextArg); + } + await this.dockerContext.detect(); + this.dockerContext.logContextInfo(); + this.dockerContext.logRootlessWarnings(); + // Load project info try { const projectinfoInstance = new plugins.projectinfo.ProjectInfo(paths.cwd); @@ -169,7 +180,7 @@ export class TsDockerManager { const useRegistry = Dockerfile.needsLocalRegistry(toBuild, options); if (useRegistry) { - await Dockerfile.startLocalRegistry(); + await Dockerfile.startLocalRegistry(this.dockerContext.contextInfo?.isRootless); } try { @@ -225,6 +236,7 @@ export class TsDockerManager { timeout: options?.timeout, noCache: options?.noCache, verbose: options?.verbose, + isRootless: this.dockerContext.contextInfo?.isRootless, }); } @@ -254,30 +266,32 @@ export class TsDockerManager { * Ensures Docker buildx is set up for multi-architecture builds */ private async ensureBuildx(): Promise { + const builderName = this.dockerContext.getBuilderName(); const platforms = this.config.platforms?.join(', ') || 'default'; - logger.log('info', `Setting up Docker buildx for multi-platform builds [${platforms}]...`); - const inspectResult = await smartshellInstance.exec('docker buildx inspect tsdocker-builder 2>/dev/null'); + logger.log('info', `Setting up Docker buildx [${platforms}]...`); + logger.log('info', `Builder: ${builderName}`); + const inspectResult = await smartshellInstance.exec(`docker buildx inspect ${builderName} 2>/dev/null`); if (inspectResult.exitCode !== 0) { logger.log('info', 'Creating new buildx builder with host network...'); await smartshellInstance.exec( - 'docker buildx create --name tsdocker-builder --driver docker-container --driver-opt network=host --use' + `docker buildx create --name ${builderName} --driver docker-container --driver-opt network=host --use` ); await smartshellInstance.exec('docker buildx inspect --bootstrap'); } else { const inspectOutput = inspectResult.stdout || ''; if (!inspectOutput.includes('network=host')) { logger.log('info', 'Recreating buildx builder with host network (migration)...'); - await smartshellInstance.exec('docker buildx rm tsdocker-builder 2>/dev/null'); + await smartshellInstance.exec(`docker buildx rm ${builderName} 2>/dev/null`); await smartshellInstance.exec( - 'docker buildx create --name tsdocker-builder --driver docker-container --driver-opt network=host --use' + `docker buildx create --name ${builderName} --driver docker-container --driver-opt network=host --use` ); await smartshellInstance.exec('docker buildx inspect --bootstrap'); } else { - await smartshellInstance.exec('docker buildx use tsdocker-builder'); + await smartshellInstance.exec(`docker buildx use ${builderName}`); } } - logger.log('ok', `Docker buildx ready (platforms: ${platforms})`); + logger.log('ok', `Docker buildx ready (builder: ${builderName}, platforms: ${platforms})`); } /** diff --git a/ts/interfaces/index.ts b/ts/interfaces/index.ts index 3751f78..e2a571a 100644 --- a/ts/interfaces/index.ts +++ b/ts/interfaces/index.ts @@ -79,6 +79,7 @@ export interface IBuildCommandOptions { noCache?: boolean; // Force rebuild without Docker layer cache (--no-cache) cached?: boolean; // Skip builds when Dockerfile content hasn't changed verbose?: boolean; // Stream raw docker build output (default: silent) + context?: string; // Explicit Docker context name (--context flag) } export interface ICacheEntry { @@ -92,3 +93,10 @@ export interface ICacheData { version: 1; entries: { [cleanTag: string]: ICacheEntry }; } + +export interface IDockerContextInfo { + name: string; // 'default', 'rootless', 'colima', etc. + endpoint: string; // 'unix:///var/run/docker.sock' + isRootless: boolean; + dockerHost?: string; // value of DOCKER_HOST env var, if set +} diff --git a/ts/tsdocker.cli.ts b/ts/tsdocker.cli.ts index 7902f10..dea831f 100644 --- a/ts/tsdocker.cli.ts +++ b/ts/tsdocker.cli.ts @@ -33,7 +33,7 @@ export let run = () => { try { const config = await ConfigModule.run(); const manager = new TsDockerManager(config); - await manager.prepare(); + await manager.prepare(argvArg.context as string | undefined); const buildOptions: IBuildCommandOptions = {}; const patterns = argvArg._.slice(1) as string[]; @@ -72,7 +72,7 @@ export let run = () => { try { const config = await ConfigModule.run(); const manager = new TsDockerManager(config); - await manager.prepare(); + await manager.prepare(argvArg.context as string | undefined); // Login first await manager.login(); @@ -124,7 +124,7 @@ export let run = () => { const config = await ConfigModule.run(); const manager = new TsDockerManager(config); - await manager.prepare(); + await manager.prepare(argvArg.context as string | undefined); // Login first await manager.login(); @@ -144,7 +144,7 @@ export let run = () => { try { const config = await ConfigModule.run(); const manager = new TsDockerManager(config); - await manager.prepare(); + await manager.prepare(argvArg.context as string | undefined); // Build images first const buildOptions: IBuildCommandOptions = {}; @@ -175,7 +175,7 @@ export let run = () => { try { const config = await ConfigModule.run(); const manager = new TsDockerManager(config); - await manager.prepare(); + await manager.prepare(argvArg.context as string | undefined); await manager.login(); logger.log('success', 'Login completed successfully'); } catch (err) { @@ -191,7 +191,7 @@ export let run = () => { try { const config = await ConfigModule.run(); const manager = new TsDockerManager(config); - await manager.prepare(); + await manager.prepare(argvArg.context as string | undefined); await manager.list(); } catch (err) { logger.log('error', `List failed: ${(err as Error).message}`);