Files
docker/ts/classes.host.ts

415 lines
11 KiB
TypeScript
Raw Normal View History

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';
2024-02-02 16:54:07 +01:00
import path from 'path';
import { DockerImageStore } from './classes.imagestore.js';
import { DockerImage } from './classes.image.js';
2018-07-16 23:52:50 +02:00
2019-09-13 22:09:35 +02:00
export interface IAuthData {
serveraddress: string;
username: string;
password: string;
}
2024-06-05 23:56:02 +02:00
export interface IDockerHostConstructorOptions {
dockerSockPath?: string;
imageStoreDir?: string;
}
2018-07-16 23:52:50 +02:00
export class DockerHost {
public options: IDockerHostConstructorOptions;
2018-07-16 23:52:50 +02:00
/**
* the path where the docker sock can be found
*/
2019-08-14 23:21:54 +02:00
public socketPath: string;
2019-09-13 17:54:17 +02:00
private registryToken: string = '';
2024-06-05 23:56:02 +02:00
public imageStore: DockerImageStore;
2024-06-10 00:15:01 +02:00
public smartBucket: plugins.smartbucket.SmartBucket;
2018-07-16 23:52:50 +02:00
/**
* the constructor to instantiate a new docker sock instance
* @param pathArg
*/
2024-06-05 23:56:02 +02:00
constructor(optionsArg: IDockerHostConstructorOptions) {
this.options = {
...{
imageStoreDir: plugins.path.join(
paths.nogitDir,
'temp-docker-image-store',
),
},
...optionsArg,
};
2019-08-16 21:21:30 +02:00
let pathToUse: string;
2024-06-05 23:56:02 +02:00
if (optionsArg.dockerSockPath) {
pathToUse = optionsArg.dockerSockPath;
2024-02-02 16:54:07 +01:00
} else if (process.env.DOCKER_HOST) {
pathToUse = process.env.DOCKER_HOST;
2019-08-16 21:21:30 +02:00
} else if (process.env.CI) {
2019-08-16 21:34:35 +02:00
pathToUse = 'http://docker:2375/';
2019-08-16 21:21:30 +02:00
} else {
pathToUse = 'http://unix:/var/run/docker.sock:';
}
2024-02-02 16:54:07 +01:00
if (pathToUse.startsWith('unix:///')) {
2024-02-02 16:55:51 +01:00
pathToUse = pathToUse.replace('unix://', 'http://unix:');
}
if (pathToUse.endsWith('.sock')) {
pathToUse = pathToUse.replace('.sock', '.sock:');
2024-02-02 16:54:07 +01:00
}
console.log(`using docker sock at ${pathToUse}`);
2019-08-16 21:21:30 +02:00
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();
2018-07-16 23:52:50 +02:00
}
/**
* authenticate against a registry
* @param userArg
* @param passArg
*/
2019-09-13 22:09:35 +02:00
public async auth(authData: IAuthData) {
const response = await this.request('POST', '/auth', authData);
2019-09-13 17:54:17 +02:00
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),
);
2019-08-14 23:21:54 +02:00
}
2019-09-13 18:15:45 +02:00
/**
* gets the token from the .docker/config.json file for GitLab registry
*/
2024-05-08 19:58:09 +02:00
public async getAuthTokenFromDockerConfig(registryUrlArg: string) {
const dockerConfigPath = plugins.smartpath.get.home(
'~/.docker/config.json',
);
2019-09-13 18:15:45 +02:00
const configObject = plugins.smartfile.fs.toObjectSync(dockerConfigPath);
2024-05-08 19:58:09 +02:00
const gitlabAuthBase64 = configObject.auths[registryUrlArg].auth;
const gitlabAuth: string =
plugins.smartstring.base64.decode(gitlabAuthBase64);
2019-09-13 22:09:35 +02:00
const gitlabAuthArray = gitlabAuth.split(':');
await this.auth({
username: gitlabAuthArray[0],
password: gitlabAuthArray[1],
serveraddress: registryUrlArg,
2019-09-18 17:29:43 +02:00
});
2019-09-13 18:15:45 +02:00
}
// ==============
// NETWORKS
// ==============
2019-08-14 23:21:54 +02:00
/**
* gets all networks
*/
public async getNetworks() {
2019-08-15 18:50:13 +02:00
return await DockerNetwork.getNetworks(this);
2018-07-16 23:52:50 +02:00
}
2019-09-13 18:15:45 +02:00
/**
* create a network
2019-09-13 18:15:45 +02:00
*/
public async createNetwork(
optionsArg: Parameters<typeof DockerNetwork.createNetwork>[1],
) {
return await DockerNetwork.createNetwork(this, optionsArg);
}
/**
* get a network by name
*/
public async getNetworkByName(networkNameArg: string) {
return await DockerNetwork.getNetworkByName(this, networkNameArg);
}
2019-09-13 18:15:45 +02:00
// ==============
// CONTAINERS
// ==============
2018-07-16 23:52:50 +02:00
/**
2019-08-14 23:21:54 +02:00
* gets all containers
2018-07-16 23:52:50 +02:00
*/
2019-08-14 23:21:54 +02:00
public async getContainers() {
2018-07-16 23:52:50 +02:00
const containerArray = await DockerContainer.getContainers(this);
return containerArray;
2019-01-10 00:28:12 +01:00
}
2019-01-10 00:24:35 +01:00
// ==============
// SERVICES
// ==============
2019-09-08 19:22:20 +02:00
/**
* 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);
}
2019-08-15 18:50:13 +02:00
/**
*
*/
2019-08-14 23:21:54 +02:00
public async getEventObservable(): Promise<plugins.rxjs.Observable<any>> {
2019-01-10 00:24:35 +01:00
const response = await this.requestStreaming('GET', '/events');
2020-09-30 16:35:24 +00:00
return plugins.rxjs.Observable.create((observer) => {
response.on('data', (data) => {
2019-01-18 02:42:13 +01:00
const eventString = data.toString();
try {
const eventObject = JSON.parse(eventString);
observer.next(eventObject);
} catch (e) {
console.log(e);
}
2019-01-10 00:24:35 +01:00
});
return () => {
response.emit('end');
};
});
2018-07-16 23:52:50 +02:00
}
2019-08-15 18:50:13 +02:00
/**
* activates docker swarm
*/
2019-08-16 12:48:40 +02:00
public async activateSwarm(addvertisementIpArg?: string) {
2019-09-08 16:34:26 +02:00
// 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;
}
}
2019-08-15 18:50:13 +02:00
const response = await this.request('POST', '/swarm/init', {
ListenAddr: '0.0.0.0:2377',
2019-09-08 16:34:26 +02:00
AdvertiseAddr: addvertisementIp,
2019-08-15 18:50:13 +02:00
DataPathPort: 4789,
DefaultAddrPool: ['10.10.0.0/8', '20.20.0.0/8'],
SubnetSize: 24,
2020-09-30 16:35:24 +00:00
ForceNewCluster: false,
2019-08-15 18:50:13 +02:00
});
if (response.statusCode === 200) {
2020-09-30 16:27:43 +00:00
logger.log('info', 'created Swam succesfully');
2019-08-15 18:50:13 +02:00
} else {
2020-09-30 16:27:43 +00:00
logger.log('error', 'could not initiate swarm');
2019-08-15 18:50:13 +02:00
}
}
2018-07-16 23:52:50 +02:00
/**
* fire a request
*/
2019-08-14 23:21:54 +02:00
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'] || '';
// Docker's streaming endpoints (like /images/create) return newline-delimited JSON
// which can't be parsed as a single JSON object
const isStreamingEndpoint =
routeArg.includes('/images/create') ||
routeArg.includes('/images/load') ||
routeArg.includes('/build');
if (contentType.includes('application/json') && !isStreamingEndpoint) {
body = await response.json();
} else {
body = await response.text();
// Try to parse as JSON if it looks like JSON and is not a streaming response
if (
!isStreamingEndpoint &&
body &&
(body.startsWith('{') || body.startsWith('['))
) {
try {
body = JSON.parse(body);
} catch {
// Keep as text if parsing fails
}
}
2019-08-15 18:50:13 +02:00
}
// 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;
2018-07-16 23:52:50 +02:00
}
2019-01-10 00:24:35 +01:00
public async requestStreaming(
methodArg: string,
routeArg: string,
readStream?: plugins.smartstream.stream.Readable,
) {
2019-08-14 23:21:54 +02:00
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')
.timeout(30000)
.options({ keepAlive: false, autoDrain: true }); // Disable auto-drain for streaming
// 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;
2019-01-10 00:24:35 +01:00
}
2024-06-10 00:15:01 +02:00
/**
* 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,
);
2024-06-10 00:15:01 +02:00
let wantedDirectory = await bucket.getBaseDirectory();
if (optionsArg.directoryPath) {
wantedDirectory = await wantedDirectory.getSubDirectoryByName(
optionsArg.directoryPath,
);
2024-06-10 00:15:01 +02:00
}
this.imageStore.options.bucketDir = wantedDirectory;
}
2018-07-16 23:52:50 +02:00
}