Compare commits

..

2 Commits

Author SHA1 Message Date
b04b8c9033 v1.13.0
Some checks failed
Default (tags) / security (push) Successful in 40s
Default (tags) / test (push) Failing after 4m1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-02-07 04:33:07 +00:00
2130a8a879 feat(docker): add Docker context detection, rootless support, and context-aware buildx registry handling 2026-02-07 04:33:07 +00:00
8 changed files with 123 additions and 20 deletions

View File

@@ -1,5 +1,14 @@
# Changelog # 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-<sanitized-context>) 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) ## 2026-02-06 - 1.12.0 - feat(docker)
add detailed logging for buildx, build commands, local registry, and local dependency info add detailed logging for buildx, build commands, local registry, and local dependency info

View File

@@ -1,6 +1,6 @@
{ {
"name": "@git.zone/tsdocker", "name": "@git.zone/tsdocker",
"version": "1.12.0", "version": "1.13.0",
"private": false, "private": false,
"description": "develop npm modules cross platform with docker", "description": "develop npm modules cross platform with docker",
"main": "dist_ts/index.js", "main": "dist_ts/index.js",

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@git.zone/tsdocker', name: '@git.zone/tsdocker',
version: '1.12.0', version: '1.13.0',
description: 'develop npm modules cross platform with docker' description: 'develop npm modules cross platform with docker'
} }

View File

@@ -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<IDockerContextInfo> {
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-<context> */
public getBuilderName(): string {
const contextName = this.contextInfo?.name || 'default';
const sanitized = contextName.replace(/[^a-zA-Z0-9_-]/g, '-');
return `tsdocker-builder-${sanitized}`;
}
}

View File

@@ -151,7 +151,7 @@ export class Dockerfile {
} }
/** Starts a temporary registry:2 container on port 5234. */ /** Starts a temporary registry:2 container on port 5234. */
public static async startLocalRegistry(): Promise<void> { public static async startLocalRegistry(isRootless?: boolean): Promise<void> {
await smartshellInstance.execSilent( await smartshellInstance.execSilent(
`docker rm -f ${LOCAL_REGISTRY_CONTAINER} 2>/dev/null || true` `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 // registry:2 starts near-instantly; brief wait for readiness
await new Promise(resolve => setTimeout(resolve, 1000)); await new Promise(resolve => setTimeout(resolve, 1000));
logger.log('info', `Started local registry at ${LOCAL_REGISTRY_HOST} (buildx dependency bridge)`); 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. */ /** Stops and removes the temporary local registry container. */
@@ -191,14 +194,14 @@ export class Dockerfile {
*/ */
public static async buildDockerfiles( public static async buildDockerfiles(
sortedArrayArg: Dockerfile[], sortedArrayArg: Dockerfile[],
options?: { platform?: string; timeout?: number; noCache?: boolean; verbose?: boolean }, options?: { platform?: string; timeout?: number; noCache?: boolean; verbose?: boolean; isRootless?: boolean },
): Promise<Dockerfile[]> { ): Promise<Dockerfile[]> {
const total = sortedArrayArg.length; const total = sortedArrayArg.length;
const overallStart = Date.now(); const overallStart = Date.now();
const useRegistry = Dockerfile.needsLocalRegistry(sortedArrayArg, options); const useRegistry = Dockerfile.needsLocalRegistry(sortedArrayArg, options);
if (useRegistry) { if (useRegistry) {
await Dockerfile.startLocalRegistry(); await Dockerfile.startLocalRegistry(options?.isRootless);
} }
try { try {

View File

@@ -5,6 +5,7 @@ import { Dockerfile } from './classes.dockerfile.js';
import { DockerRegistry } from './classes.dockerregistry.js'; import { DockerRegistry } from './classes.dockerregistry.js';
import { RegistryStorage } from './classes.registrystorage.js'; import { RegistryStorage } from './classes.registrystorage.js';
import { TsDockerCache } from './classes.tsdockercache.js'; import { TsDockerCache } from './classes.tsdockercache.js';
import { DockerContext } from './classes.dockercontext.js';
import type { ITsDockerConfig, IBuildCommandOptions } from './interfaces/index.js'; import type { ITsDockerConfig, IBuildCommandOptions } from './interfaces/index.js';
const smartshellInstance = new plugins.smartshell.Smartshell({ const smartshellInstance = new plugins.smartshell.Smartshell({
@@ -18,17 +19,27 @@ export class TsDockerManager {
public registryStorage: RegistryStorage; public registryStorage: RegistryStorage;
public config: ITsDockerConfig; public config: ITsDockerConfig;
public projectInfo: any; public projectInfo: any;
public dockerContext: DockerContext;
private dockerfiles: Dockerfile[] = []; private dockerfiles: Dockerfile[] = [];
constructor(config: ITsDockerConfig) { constructor(config: ITsDockerConfig) {
this.config = config; this.config = config;
this.registryStorage = new RegistryStorage(); this.registryStorage = new RegistryStorage();
this.dockerContext = new DockerContext();
} }
/** /**
* Prepares the manager by loading project info and registries * Prepares the manager by loading project info and registries
*/ */
public async prepare(): Promise<void> { public async prepare(contextArg?: string): Promise<void> {
// Detect Docker context
if (contextArg) {
this.dockerContext.setContext(contextArg);
}
await this.dockerContext.detect();
this.dockerContext.logContextInfo();
this.dockerContext.logRootlessWarnings();
// Load project info // Load project info
try { try {
const projectinfoInstance = new plugins.projectinfo.ProjectInfo(paths.cwd); const projectinfoInstance = new plugins.projectinfo.ProjectInfo(paths.cwd);
@@ -169,7 +180,7 @@ export class TsDockerManager {
const useRegistry = Dockerfile.needsLocalRegistry(toBuild, options); const useRegistry = Dockerfile.needsLocalRegistry(toBuild, options);
if (useRegistry) { if (useRegistry) {
await Dockerfile.startLocalRegistry(); await Dockerfile.startLocalRegistry(this.dockerContext.contextInfo?.isRootless);
} }
try { try {
@@ -225,6 +236,7 @@ export class TsDockerManager {
timeout: options?.timeout, timeout: options?.timeout,
noCache: options?.noCache, noCache: options?.noCache,
verbose: options?.verbose, 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 * Ensures Docker buildx is set up for multi-architecture builds
*/ */
private async ensureBuildx(): Promise<void> { private async ensureBuildx(): Promise<void> {
const builderName = this.dockerContext.getBuilderName();
const platforms = this.config.platforms?.join(', ') || 'default'; const platforms = this.config.platforms?.join(', ') || 'default';
logger.log('info', `Setting up Docker buildx for multi-platform builds [${platforms}]...`); logger.log('info', `Setting up Docker buildx [${platforms}]...`);
const inspectResult = await smartshellInstance.exec('docker buildx inspect tsdocker-builder 2>/dev/null'); logger.log('info', `Builder: ${builderName}`);
const inspectResult = await smartshellInstance.exec(`docker buildx inspect ${builderName} 2>/dev/null`);
if (inspectResult.exitCode !== 0) { if (inspectResult.exitCode !== 0) {
logger.log('info', 'Creating new buildx builder with host network...'); logger.log('info', 'Creating new buildx builder with host network...');
await smartshellInstance.exec( 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'); await smartshellInstance.exec('docker buildx inspect --bootstrap');
} else { } else {
const inspectOutput = inspectResult.stdout || ''; const inspectOutput = inspectResult.stdout || '';
if (!inspectOutput.includes('network=host')) { if (!inspectOutput.includes('network=host')) {
logger.log('info', 'Recreating buildx builder with host network (migration)...'); 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( 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'); await smartshellInstance.exec('docker buildx inspect --bootstrap');
} else { } 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})`);
} }
/** /**

View File

@@ -79,6 +79,7 @@ export interface IBuildCommandOptions {
noCache?: boolean; // Force rebuild without Docker layer cache (--no-cache) noCache?: boolean; // Force rebuild without Docker layer cache (--no-cache)
cached?: boolean; // Skip builds when Dockerfile content hasn't changed cached?: boolean; // Skip builds when Dockerfile content hasn't changed
verbose?: boolean; // Stream raw docker build output (default: silent) verbose?: boolean; // Stream raw docker build output (default: silent)
context?: string; // Explicit Docker context name (--context flag)
} }
export interface ICacheEntry { export interface ICacheEntry {
@@ -92,3 +93,10 @@ export interface ICacheData {
version: 1; version: 1;
entries: { [cleanTag: string]: ICacheEntry }; 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
}

View File

@@ -33,7 +33,7 @@ export let run = () => {
try { try {
const config = await ConfigModule.run(); const config = await ConfigModule.run();
const manager = new TsDockerManager(config); const manager = new TsDockerManager(config);
await manager.prepare(); await manager.prepare(argvArg.context as string | undefined);
const buildOptions: IBuildCommandOptions = {}; const buildOptions: IBuildCommandOptions = {};
const patterns = argvArg._.slice(1) as string[]; const patterns = argvArg._.slice(1) as string[];
@@ -72,7 +72,7 @@ export let run = () => {
try { try {
const config = await ConfigModule.run(); const config = await ConfigModule.run();
const manager = new TsDockerManager(config); const manager = new TsDockerManager(config);
await manager.prepare(); await manager.prepare(argvArg.context as string | undefined);
// Login first // Login first
await manager.login(); await manager.login();
@@ -124,7 +124,7 @@ export let run = () => {
const config = await ConfigModule.run(); const config = await ConfigModule.run();
const manager = new TsDockerManager(config); const manager = new TsDockerManager(config);
await manager.prepare(); await manager.prepare(argvArg.context as string | undefined);
// Login first // Login first
await manager.login(); await manager.login();
@@ -144,7 +144,7 @@ export let run = () => {
try { try {
const config = await ConfigModule.run(); const config = await ConfigModule.run();
const manager = new TsDockerManager(config); const manager = new TsDockerManager(config);
await manager.prepare(); await manager.prepare(argvArg.context as string | undefined);
// Build images first // Build images first
const buildOptions: IBuildCommandOptions = {}; const buildOptions: IBuildCommandOptions = {};
@@ -175,7 +175,7 @@ export let run = () => {
try { try {
const config = await ConfigModule.run(); const config = await ConfigModule.run();
const manager = new TsDockerManager(config); const manager = new TsDockerManager(config);
await manager.prepare(); await manager.prepare(argvArg.context as string | undefined);
await manager.login(); await manager.login();
logger.log('success', 'Login completed successfully'); logger.log('success', 'Login completed successfully');
} catch (err) { } catch (err) {
@@ -191,7 +191,7 @@ export let run = () => {
try { try {
const config = await ConfigModule.run(); const config = await ConfigModule.run();
const manager = new TsDockerManager(config); const manager = new TsDockerManager(config);
await manager.prepare(); await manager.prepare(argvArg.context as string | undefined);
await manager.list(); await manager.list();
} catch (err) { } catch (err) {
logger.log('error', `List failed: ${(err as Error).message}`); logger.log('error', `List failed: ${(err as Error).message}`);