fix(deps): upgrade core tooling dependencies and adapt Docker client internals for compatibility

This commit is contained in:
2026-03-28 05:39:48 +00:00
parent 1923837225
commit 645e1fd4a9
19 changed files with 5861 additions and 7164 deletions

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@apiclient.xyz/docker',
version: '5.1.1',
version: '5.1.2',
description: 'Provides easy communication with Docker remote API from Node.js, with TypeScript support.'
}

View File

@@ -62,7 +62,11 @@ export class DockerContainer extends DockerResource {
if (response.statusCode < 300) {
logger.log('info', 'Container created successfully');
// Return the created container instance
return await DockerContainer._fromId(dockerHost, response.body.Id);
const container = await DockerContainer._fromId(dockerHost, response.body.Id);
if (!container) {
throw new Error('Container was created but could not be retrieved');
}
return container;
} else {
logger.log('error', 'There has been a problem when creating the container');
throw new Error(`Failed to create container: ${response.statusCode}`);
@@ -70,18 +74,18 @@ export class DockerContainer extends DockerResource {
}
// INSTANCE PROPERTIES
public Id: string;
public Names: string[];
public Image: string;
public ImageID: string;
public Command: string;
public Created: number;
public Ports: interfaces.TPorts;
public Labels: interfaces.TLabels;
public State: string;
public Status: string;
public Id!: string;
public Names!: string[];
public Image!: string;
public ImageID!: string;
public Command!: string;
public Created!: number;
public Ports!: interfaces.TPorts;
public Labels!: interfaces.TLabels;
public State!: string;
public Status!: string;
public HostConfig: any;
public NetworkSettings: {
public NetworkSettings!: {
Networks: {
[key: string]: {
IPAMConfig: any;

View File

@@ -29,7 +29,7 @@ export class DockerHost {
public socketPath: string;
private registryToken: string = '';
private imageStore: DockerImageStore; // Now private - use storeImage/retrieveImage instead
public smartBucket: plugins.smartbucket.SmartBucket;
public smartBucket!: plugins.smartbucket.SmartBucket;
/**
* the constructor to instantiate a new docker sock instance
@@ -64,8 +64,8 @@ export class DockerHost {
console.log(`using docker sock at ${pathToUse}`);
this.socketPath = pathToUse;
this.imageStore = new DockerImageStore({
bucketDir: null,
localDirPath: this.options.imageStoreDir,
bucketDir: null!,
localDirPath: this.options.imageStoreDir!,
});
}
@@ -74,6 +74,9 @@ export class DockerHost {
}
public async stop() {
await this.imageStore.stop();
if (this.smartBucket) {
this.smartBucket.storageClient.destroy();
}
}
/**
@@ -131,7 +134,7 @@ export class DockerHost {
const dockerConfigPath = plugins.smartpath.get.home(
'~/.docker/config.json',
);
const configObject = plugins.smartfile.fs.toObjectSync(dockerConfigPath);
const configObject = JSON.parse(plugins.fs.readFileSync(dockerConfigPath, 'utf8'));
const gitlabAuthBase64 = configObject.auths[registryUrlArg].auth;
const gitlabAuth: string =
plugins.smartstring.base64.decode(gitlabAuthBase64);
@@ -379,8 +382,14 @@ export class DockerHost {
console.log(e);
}
});
nodeStream.on('error', (err) => {
// Connection resets are expected when the stream is destroyed
if ((err as any).code !== 'ECONNRESET') {
observer.error(err);
}
});
return () => {
nodeStream.emit('end');
nodeStream.destroy();
};
});
}
@@ -390,14 +399,19 @@ export class DockerHost {
*/
public async activateSwarm(addvertisementIpArg?: string) {
// determine advertisement address
let addvertisementIp: string;
let addvertisementIp: string = '';
if (addvertisementIpArg) {
addvertisementIp = addvertisementIpArg;
} else {
const smartnetworkInstance = new plugins.smartnetwork.SmartNetwork();
const defaultGateway = await smartnetworkInstance.getDefaultGateway();
if (defaultGateway) {
addvertisementIp = defaultGateway.ipv4.address;
try {
const smartnetworkInstance = new plugins.smartnetwork.SmartNetwork();
const defaultGateway = await smartnetworkInstance.getDefaultGateway();
if (defaultGateway) {
addvertisementIp = defaultGateway.ipv4.address;
}
} catch (err) {
// Failed to determine default gateway (e.g. in Deno without --allow-run)
// Docker will auto-detect the advertise address
}
}
@@ -502,7 +516,7 @@ export class DockerHost {
routeArg: string,
readStream?: plugins.smartstream.stream.Readable,
jsonData?: any,
) {
): Promise<plugins.smartstream.stream.Readable | { statusCode: number; body: string; headers: any }> {
const requestUrl = `${this.socketPath}${routeArg}`;
// Build the request using the fluent API
@@ -579,6 +593,10 @@ export class DockerHost {
// Convert web ReadableStream to Node.js stream for backward compatibility
const nodeStream = plugins.smartstream.nodewebhelpers.convertWebReadableToNodeReadable(webStream);
// Add a default error handler to prevent unhandled 'error' events from crashing the process.
// Callers that attach their own 'error' listener will still receive the event.
nodeStream.on('error', () => {});
// Add properties for compatibility
(nodeStream as any).statusCode = response.status;
(nodeStream as any).body = ''; // For compatibility

View File

@@ -59,8 +59,8 @@ export class DockerImage extends DockerResource {
imageOriginTag: string;
} = {
imageUrl: optionsArg.creationObject.imageUrl,
imageTag: optionsArg.creationObject.imageTag,
imageOriginTag: null,
imageTag: optionsArg.creationObject.imageTag ?? '',
imageOriginTag: '',
};
if (imageUrlObject.imageUrl.includes(':')) {
const imageUrl = imageUrlObject.imageUrl.split(':')[0];
@@ -94,9 +94,24 @@ export class DockerImage extends DockerResource {
dockerHostArg,
imageUrlObject.imageOriginTag,
);
if (!image) {
throw new Error(`Image ${imageUrlObject.imageOriginTag} not found after pull`);
}
return image;
} else {
logger.log('error', `Failed at the attempt of creating a new image`);
// Pull failed — check if the image already exists locally
const existingImage = await DockerImage._fromName(
dockerHostArg,
imageUrlObject.imageOriginTag,
);
if (existingImage) {
logger.log(
'warn',
`Pull failed for ${imageUrlObject.imageUrl}, using locally cached image`,
);
return existingImage;
}
throw new Error(`Failed to pull image ${imageUrlObject.imageOriginTag} and no local copy exists`);
}
}
@@ -217,16 +232,16 @@ export class DockerImage extends DockerResource {
/**
* the tags for an image
*/
public Containers: number;
public Created: number;
public Id: string;
public Labels: interfaces.TLabels;
public ParentId: string;
public RepoDigests: string[];
public RepoTags: string[];
public SharedSize: number;
public Size: number;
public VirtualSize: number;
public Containers!: number;
public Created!: number;
public Id!: string;
public Labels!: interfaces.TLabels;
public ParentId!: string;
public RepoDigests!: string[];
public RepoTags!: string[];
public SharedSize!: number;
public Size!: number;
public VirtualSize!: number;
constructor(dockerHostArg: DockerHost, dockerImageObjectArg: any) {
super(dockerHostArg);

View File

@@ -3,6 +3,8 @@ import * as paths from './paths.js';
import { logger } from './logger.js';
import type { DockerHost } from './classes.host.js';
const smartfileFactory = plugins.smartfile.SmartFileFactory.nodeFs();
export interface IDockerImageStoreConstructorOptions {
/**
* used for preparing images for longer term storage
@@ -38,14 +40,12 @@ export class DockerImageStore {
uniqueProcessingId,
);
// Create a write stream to store the tar file
const writeStream = plugins.smartfile.fsStream.createWriteStream(
initialTarDownloadPath,
);
const writeStream = plugins.fs.createWriteStream(initialTarDownloadPath);
// lets wait for the write stream to finish
await new Promise((resolve, reject) => {
await new Promise<void>((resolve, reject) => {
tarStream.pipe(writeStream);
writeStream.on('finish', resolve);
writeStream.on('finish', () => resolve());
writeStream.on('error', reject);
});
logger.log(
@@ -54,44 +54,55 @@ export class DockerImageStore {
);
// lets process the image
const tarArchive = await plugins.smartarchive.SmartArchive.fromArchiveFile(
initialTarDownloadPath,
);
await tarArchive.exportToFs(extractionDir);
await plugins.smartarchive.SmartArchive.create()
.file(initialTarDownloadPath)
.extract(extractionDir);
logger.log('info', `Image ${imageName} extracted.`);
await plugins.smartfile.fs.remove(initialTarDownloadPath);
await plugins.fs.promises.rm(initialTarDownloadPath, { force: true });
logger.log('info', `deleted original tar to save space.`);
logger.log('info', `now repackaging for s3...`);
const smartfileIndexJson = await plugins.smartfile.SmartFile.fromFilePath(
const smartfileIndexJson = await smartfileFactory.fromFilePath(
plugins.path.join(extractionDir, 'index.json'),
);
const smartfileManifestJson =
await plugins.smartfile.SmartFile.fromFilePath(
plugins.path.join(extractionDir, 'manifest.json'),
);
const smartfileOciLayoutJson =
await plugins.smartfile.SmartFile.fromFilePath(
plugins.path.join(extractionDir, 'oci-layout'),
);
const smartfileRepositoriesJson =
await plugins.smartfile.SmartFile.fromFilePath(
plugins.path.join(extractionDir, 'repositories'),
);
const smartfileManifestJson = await smartfileFactory.fromFilePath(
plugins.path.join(extractionDir, 'manifest.json'),
);
const smartfileOciLayoutJson = await smartfileFactory.fromFilePath(
plugins.path.join(extractionDir, 'oci-layout'),
);
// repositories file is optional in OCI image tars
const repositoriesPath = plugins.path.join(extractionDir, 'repositories');
const hasRepositories = plugins.fs.existsSync(repositoriesPath);
const smartfileRepositoriesJson = hasRepositories
? await smartfileFactory.fromFilePath(repositoriesPath)
: null;
const indexJson = JSON.parse(smartfileIndexJson.contents.toString());
const manifestJson = JSON.parse(smartfileManifestJson.contents.toString());
const ociLayoutJson = JSON.parse(
smartfileOciLayoutJson.contents.toString(),
);
const repositoriesJson = JSON.parse(
smartfileRepositoriesJson.contents.toString(),
);
indexJson.manifests[0].annotations['io.containerd.image.name'] = imageName;
manifestJson[0].RepoTags[0] = imageName;
const repoFirstKey = Object.keys(repositoriesJson)[0];
const repoFirstValue = repositoriesJson[repoFirstKey];
repositoriesJson[imageName] = repoFirstValue;
delete repositoriesJson[repoFirstKey];
if (indexJson.manifests?.[0]?.annotations) {
indexJson.manifests[0].annotations['io.containerd.image.name'] = imageName;
}
if (manifestJson?.[0]?.RepoTags) {
manifestJson[0].RepoTags[0] = imageName;
}
if (smartfileRepositoriesJson) {
const repositoriesJson = JSON.parse(
smartfileRepositoriesJson.contents.toString(),
);
const repoFirstKey = Object.keys(repositoriesJson)[0];
const repoFirstValue = repositoriesJson[repoFirstKey];
repositoriesJson[imageName] = repoFirstValue;
delete repositoriesJson[repoFirstKey];
smartfileRepositoriesJson.contents = Buffer.from(
JSON.stringify(repositoriesJson, null, 2),
);
}
smartfileIndexJson.contents = Buffer.from(
JSON.stringify(indexJson, null, 2),
@@ -102,45 +113,51 @@ export class DockerImageStore {
smartfileOciLayoutJson.contents = Buffer.from(
JSON.stringify(ociLayoutJson, null, 2),
);
smartfileRepositoriesJson.contents = Buffer.from(
JSON.stringify(repositoriesJson, null, 2),
);
await Promise.all([
const writePromises = [
smartfileIndexJson.write(),
smartfileManifestJson.write(),
smartfileOciLayoutJson.write(),
smartfileRepositoriesJson.write(),
]);
];
if (smartfileRepositoriesJson) {
writePromises.push(smartfileRepositoriesJson.write());
}
await Promise.all(writePromises);
logger.log('info', 'repackaging archive for s3...');
const tartools = new plugins.smartarchive.TarTools();
const newTarPack = await tartools.packDirectory(extractionDir);
const newTarPack = await tartools.getDirectoryPackStream(extractionDir);
const finalTarName = `${uniqueProcessingId}.processed.tar`;
const finalTarPath = plugins.path.join(
this.options.localDirPath,
finalTarName,
);
const finalWriteStream =
plugins.smartfile.fsStream.createWriteStream(finalTarPath);
await new Promise((resolve, reject) => {
newTarPack.finalize();
const finalWriteStream = plugins.fs.createWriteStream(finalTarPath);
await new Promise<void>((resolve, reject) => {
newTarPack.pipe(finalWriteStream);
finalWriteStream.on('finish', resolve);
finalWriteStream.on('finish', () => resolve());
finalWriteStream.on('error', reject);
});
logger.log('ok', `Repackaged image ${imageName} for s3.`);
await plugins.smartfile.fs.remove(extractionDir);
const finalTarReadStream =
plugins.smartfile.fsStream.createReadStream(finalTarPath);
await plugins.fs.promises.rm(extractionDir, { recursive: true, force: true });
// Remove existing file in bucket if it exists (smartbucket v4 no longer silently overwrites)
try {
await this.options.bucketDir.fastRemove({ path: `${imageName}.tar` });
} catch (e) {
// File may not exist, which is fine
}
const finalTarReadStream = plugins.fs.createReadStream(finalTarPath);
await this.options.bucketDir.fastPutStream({
stream: finalTarReadStream,
path: `${imageName}.tar`,
});
await plugins.smartfile.fs.remove(finalTarPath);
await plugins.fs.promises.rm(finalTarPath, { force: true });
}
public async start() {
await plugins.smartfile.fs.ensureEmptyDir(this.options.localDirPath);
// Ensure the local directory exists and is empty
await plugins.fs.promises.rm(this.options.localDirPath, { recursive: true, force: true });
await plugins.fs.promises.mkdir(this.options.localDirPath, { recursive: true });
}
public async stop() {}
@@ -154,10 +171,10 @@ export class DockerImageStore {
`${imageName}.tar`,
);
if (!(await plugins.smartfile.fs.fileExists(imagePath))) {
if (!plugins.fs.existsSync(imagePath)) {
throw new Error(`Image ${imageName} does not exist.`);
}
return plugins.smartfile.fsStream.createReadStream(imagePath);
return plugins.fs.createReadStream(imagePath);
}
}

View File

@@ -61,30 +61,30 @@ export class DockerNetwork extends DockerResource {
});
if (response.statusCode < 300) {
logger.log('info', 'Created network successfully');
return await DockerNetwork._fromName(
const network = await DockerNetwork._fromName(
dockerHost,
networkCreationDescriptor.Name,
);
if (!network) {
throw new Error('Network was created but could not be retrieved');
}
return network;
} else {
logger.log(
'error',
'There has been an error creating the wanted network',
);
return null;
throw new Error('There has been an error creating the wanted network');
}
}
// INSTANCE PROPERTIES
public Name: string;
public Id: string;
public Created: string;
public Scope: string;
public Driver: string;
public EnableIPv6: boolean;
public Internal: boolean;
public Attachable: boolean;
public Ingress: false;
public IPAM: {
public Name!: string;
public Id!: string;
public Created!: string;
public Scope!: string;
public Driver!: string;
public EnableIPv6!: boolean;
public Internal!: boolean;
public Attachable!: boolean;
public Ingress!: false;
public IPAM!: {
Driver: 'default' | 'bridge' | 'overlay';
Config: [
{
@@ -130,7 +130,7 @@ export class DockerNetwork extends DockerResource {
IPv6Address: string;
}>
> {
const returnArray = [];
const returnArray: any[] = [];
const response = await this.dockerHost.request(
'GET',
`/networks/${this.Id}`,

View File

@@ -72,12 +72,12 @@ export class DockerSecret extends DockerResource {
}
// INSTANCE PROPERTIES
public ID: string;
public Spec: {
public ID!: string;
public Spec!: {
Name: string;
Labels: interfaces.TLabels;
};
public Version: {
public Version!: {
Index: string;
};
@@ -101,7 +101,6 @@ export class DockerSecret extends DockerResource {
* Updates a secret
*/
public async update(contentArg: string) {
const route = `/secrets/${this.ID}/update?=version=${this.Version.Index}`;
const response = await this.dockerHost.request(
'POST',
`/secrets/${this.ID}/update?version=${this.Version.Index}`,

View File

@@ -37,6 +37,9 @@ export class DockerService extends DockerResource {
const wantedService = allServices.find((service) => {
return service.Spec.Name === networkName;
});
if (!wantedService) {
throw new Error(`Service not found: ${networkName}`);
}
return wantedService;
}
@@ -56,10 +59,11 @@ export class DockerService extends DockerResource {
// Resolve image (support both string and DockerImage instance)
let imageInstance: DockerImage;
if (typeof serviceCreationDescriptor.image === 'string') {
imageInstance = await DockerImage._fromName(dockerHost, serviceCreationDescriptor.image);
if (!imageInstance) {
const foundImage = await DockerImage._fromName(dockerHost, serviceCreationDescriptor.image);
if (!foundImage) {
throw new Error(`Image not found: ${serviceCreationDescriptor.image}`);
}
imageInstance = foundImage;
} else {
imageInstance = serviceCreationDescriptor.image;
}
@@ -131,7 +135,7 @@ export class DockerService extends DockerResource {
});
}
const ports = [];
const ports: Array<{ Protocol: string; PublishedPort: number; TargetPort: number }> = [];
for (const port of serviceCreationDescriptor.ports) {
const portArray = port.split(':');
const hostPort = portArray[0];
@@ -149,10 +153,11 @@ export class DockerService extends DockerResource {
// Resolve secret instance
let secretInstance: DockerSecret;
if (typeof secret === 'string') {
secretInstance = await DockerSecret._fromName(dockerHost, secret);
if (!secretInstance) {
const foundSecret = await DockerSecret._fromName(dockerHost, secret);
if (!foundSecret) {
throw new Error(`Secret not found: ${secret}`);
}
secretInstance = foundSecret;
} else {
secretInstance = secret;
}
@@ -171,21 +176,12 @@ export class DockerService extends DockerResource {
// lets configure limits
const memoryLimitMB =
serviceCreationDescriptor.resources &&
serviceCreationDescriptor.resources.memorySizeMB
? serviceCreationDescriptor.resources.memorySizeMB
: 1000;
const memoryLimitMB = serviceCreationDescriptor.resources?.memorySizeMB ?? 1000;
const limits = {
MemoryBytes: memoryLimitMB * 1000000,
};
if (serviceCreationDescriptor.resources) {
limits.MemoryBytes =
serviceCreationDescriptor.resources.memorySizeMB * 1000000;
}
const response = await dockerHost.request('POST', '/services/create', {
Name: serviceCreationDescriptor.name,
TaskTemplate: {
@@ -234,11 +230,11 @@ export class DockerService extends DockerResource {
// INSTANCE PROPERTIES
// Note: dockerHost (not dockerHostRef) for consistency with base class
public ID: string;
public Version: { Index: number };
public CreatedAt: string;
public UpdatedAt: string;
public Spec: {
public ID!: string;
public Version!: { Index: number };
public CreatedAt!: string;
public UpdatedAt!: string;
public Spec!: {
Name: string;
Labels: interfaces.TLabels;
TaskTemplate: {
@@ -261,7 +257,7 @@ export class DockerService extends DockerResource {
Mode: {};
Networks: [any[]];
};
public Endpoint: { Spec: {}; VirtualIPs: [any[]] };
public Endpoint!: { Spec: {}; VirtualIPs: [any[]] };
constructor(dockerHostArg: DockerHost) {
super(dockerHostArg);
@@ -325,6 +321,7 @@ export class DockerService extends DockerResource {
return true;
} else {
console.log(`service ${this.Spec.Name} is up to date.`);
return false;
}
}
}

View File

@@ -1,13 +1,15 @@
// node native path
// node native
import * as fs from 'node:fs';
import * as path from 'node:path';
export { path };
export { fs, path };
// @pushrocks scope
import * as lik from '@push.rocks/lik';
import * as smartarchive from '@push.rocks/smartarchive';
import * as smartbucket from '@push.rocks/smartbucket';
import * as smartfile from '@push.rocks/smartfile';
import * as smartjson from '@push.rocks/smartjson';
import * as smartlog from '@push.rocks/smartlog';
import * as smartnetwork from '@push.rocks/smartnetwork';
@@ -24,6 +26,7 @@ export {
smartarchive,
smartbucket,
smartfile,
smartjson,
smartlog,
smartnetwork,