import * as plugins from './plugins.js'; import * as paths from './paths.js'; import { DockerContainer } from './classes.container.js'; import { DockerNetwork } from './classes.network.js'; import { DockerService } from './classes.service.js'; import { logger } from './logger.js'; import path from 'path'; import { DockerImageStore } from './classes.imagestore.js'; import { DockerImage } from './classes.image.js'; export interface IAuthData { serveraddress: string; username: string; password: string; } export interface IDockerHostConstructorOptions { dockerSockPath?: string; imageStoreDir?: string; } export class DockerHost { public options: IDockerHostConstructorOptions; /** * the path where the docker sock can be found */ public socketPath: string; private registryToken: string = ''; public imageStore: DockerImageStore; public smartBucket: plugins.smartbucket.SmartBucket; /** * the constructor to instantiate a new docker sock instance * @param pathArg */ constructor(optionsArg: IDockerHostConstructorOptions) { this.options = { ...{ imageStoreDir: plugins.path.join(paths.nogitDir, 'temp-docker-image-store'), }, ...optionsArg, } let pathToUse: string; if (optionsArg.dockerSockPath) { pathToUse = optionsArg.dockerSockPath; } else if (process.env.DOCKER_HOST) { pathToUse = process.env.DOCKER_HOST; } else if (process.env.CI) { pathToUse = 'http://docker:2375/'; } else { pathToUse = 'http://unix:/var/run/docker.sock:'; } if (pathToUse.startsWith('unix:///')) { pathToUse = pathToUse.replace('unix://', 'http://unix:'); } if (pathToUse.endsWith('.sock')) { pathToUse = pathToUse.replace('.sock', '.sock:'); } console.log(`using docker sock at ${pathToUse}`); this.socketPath = pathToUse; this.imageStore = new DockerImageStore({ bucketDir: null, localDirPath: this.options.imageStoreDir, }) } public async start() { await this.imageStore.start(); } public async stop() { await this.imageStore.stop(); } /** * authenticate against a registry * @param userArg * @param passArg */ public async auth(authData: IAuthData) { const response = await this.request('POST', '/auth', authData); if (response.body.Status !== 'Login Succeeded') { console.log(`Login failed with ${response.body.Status}`); throw new Error(response.body.Status); } console.log(response.body.Status); this.registryToken = plugins.smartstring.base64.encode(plugins.smartjson.stringify(authData)); } /** * gets the token from the .docker/config.json file for GitLab registry */ public async getAuthTokenFromDockerConfig(registryUrlArg: string) { const dockerConfigPath = plugins.smartpath.get.home('~/.docker/config.json'); const configObject = plugins.smartfile.fs.toObjectSync(dockerConfigPath); const gitlabAuthBase64 = configObject.auths[registryUrlArg].auth; const gitlabAuth: string = plugins.smartstring.base64.decode(gitlabAuthBase64); const gitlabAuthArray = gitlabAuth.split(':'); await this.auth({ username: gitlabAuthArray[0], password: gitlabAuthArray[1], serveraddress: registryUrlArg, }); } // ============== // NETWORKS // ============== /** * gets all networks */ public async getNetworks() { return await DockerNetwork.getNetworks(this); } /** * create a network */ public async createNetwork(optionsArg: Parameters[1]) { return await DockerNetwork.createNetwork(this, optionsArg); } /** * get a network by name */ public async getNetworkByName(networkNameArg: string) { return await DockerNetwork.getNetworkByName(this, networkNameArg); } // ============== // CONTAINERS // ============== /** * gets all containers */ public async getContainers() { const containerArray = await DockerContainer.getContainers(this); return containerArray; } // ============== // SERVICES // ============== /** * gets all services */ public async getServices() { const serviceArray = await DockerService.getServices(this); return serviceArray; } // ============== // IMAGES // ============== /** * get all images */ public async getImages() { return await DockerImage.getImages(this); } /** * get an image by name */ public async getImageByName(imageNameArg: string) { return await DockerImage.getImageByName(this, imageNameArg); } /** * */ public async getEventObservable(): Promise> { const response = await this.requestStreaming('GET', '/events'); return plugins.rxjs.Observable.create((observer) => { response.on('data', (data) => { const eventString = data.toString(); try { const eventObject = JSON.parse(eventString); observer.next(eventObject); } catch (e) { console.log(e); } }); return () => { response.emit('end'); }; }); } /** * activates docker swarm */ public async activateSwarm(addvertisementIpArg?: string) { // determine advertisement address 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; } } const response = await this.request('POST', '/swarm/init', { ListenAddr: '0.0.0.0:2377', AdvertiseAddr: addvertisementIp, DataPathPort: 4789, DefaultAddrPool: ['10.10.0.0/8', '20.20.0.0/8'], SubnetSize: 24, ForceNewCluster: false, }); if (response.statusCode === 200) { logger.log('info', 'created Swam succesfully'); } else { logger.log('error', 'could not initiate swarm'); } } /** * fire a request */ public async request(methodArg: string, routeArg: string, dataArg = {}) { const requestUrl = `${this.socketPath}${routeArg}`; // Build the request using the fluent API const smartRequest = plugins.smartrequest.SmartRequest.create() .url(requestUrl) .header('Content-Type', 'application/json') .header('X-Registry-Auth', this.registryToken) .header('Host', 'docker.sock') .options({ keepAlive: false }); // Add body for methods that support it if (dataArg && Object.keys(dataArg).length > 0) { smartRequest.json(dataArg); } // Execute the request based on method let response; switch (methodArg.toUpperCase()) { case 'GET': response = await smartRequest.get(); break; case 'POST': response = await smartRequest.post(); break; case 'PUT': response = await smartRequest.put(); break; case 'DELETE': response = await smartRequest.delete(); break; default: throw new Error(`Unsupported HTTP method: ${methodArg}`); } // Parse the response body based on content type let body; const contentType = response.headers['content-type'] || ''; if (contentType.includes('application/json')) { body = await response.json(); } else { body = await response.text(); // Try to parse as JSON if it looks like JSON if (body && (body.startsWith('{') || body.startsWith('['))) { try { body = JSON.parse(body); } catch { // Keep as text if parsing fails } } } // Create a response object compatible with existing code const legacyResponse = { statusCode: response.status, body: body, headers: response.headers }; if (response.status !== 200) { console.log(body); } return legacyResponse; } public async requestStreaming(methodArg: string, routeArg: string, readStream?: plugins.smartstream.stream.Readable) { const requestUrl = `${this.socketPath}${routeArg}`; // Build the request using the fluent API const smartRequest = plugins.smartrequest.SmartRequest.create() .url(requestUrl) .header('Content-Type', 'application/json') .header('X-Registry-Auth', this.registryToken) .header('Host', 'docker.sock') .options({ keepAlive: false }); // If we have a readStream, use the new stream method with logging if (readStream) { let counter = 0; const smartduplex = new plugins.smartstream.SmartDuplex({ writeFunction: async (chunkArg) => { if (counter % 1000 === 0) { console.log(`posting chunk ${counter}`); } counter++; return chunkArg; } }); // Pipe through the logging duplex stream const loggedStream = readStream.pipe(smartduplex); // Use the new stream method to stream the data smartRequest.stream(loggedStream, 'application/octet-stream'); } // Execute the request based on method let response; switch (methodArg.toUpperCase()) { case 'GET': response = await smartRequest.get(); break; case 'POST': response = await smartRequest.post(); break; case 'PUT': response = await smartRequest.put(); break; case 'DELETE': response = await smartRequest.delete(); break; default: throw new Error(`Unsupported HTTP method: ${methodArg}`); } console.log(response.status); // For streaming responses, get the Node.js stream const nodeStream = response.streamNode(); if (!nodeStream) { // If no stream is available, consume the body as text const body = await response.text(); console.log(body); // Return a compatible response object return { statusCode: response.status, body: body, headers: response.headers }; } // For streaming responses, return the stream with added properties (nodeStream as any).statusCode = response.status; (nodeStream as any).body = ''; // For compatibility return nodeStream; } /** * add s3 storage * @param optionsArg */ public async addS3Storage(optionsArg: plugins.tsclass.storage.IS3Descriptor) { this.smartBucket = new plugins.smartbucket.SmartBucket(optionsArg); if (!optionsArg.bucketName) { throw new Error('bucketName is required'); } const bucket = await this.smartBucket.getBucketByName(optionsArg.bucketName); let wantedDirectory = await bucket.getBaseDirectory(); if (optionsArg.directoryPath) { wantedDirectory = await wantedDirectory.getSubDirectoryByName(optionsArg.directoryPath); } this.imageStore.options.bucketDir = wantedDirectory; } }