import * as plugins from './plugins.js'; import * as interfaces from './interfaces/index.js'; import { DockerHost } from './classes.host.js'; import { DockerResource } from './classes.base.js'; import { DockerImage } from './classes.image.js'; import { DockerSecret } from './classes.secret.js'; import { logger } from './logger.js'; export class DockerService extends DockerResource { // STATIC (Internal - prefixed with _ to indicate internal use) /** * Internal: Get all services * Public API: Use dockerHost.listServices() instead */ public static async _list(dockerHost: DockerHost) { const services: DockerService[] = []; const response = await dockerHost.request('GET', '/services'); for (const serviceObject of response.body) { const dockerService = new DockerService(dockerHost); Object.assign(dockerService, serviceObject); services.push(dockerService); } return services; } /** * Internal: Get service by name * Public API: Use dockerHost.getServiceByName(name) instead */ public static async _fromName( dockerHost: DockerHost, networkName: string, ): Promise { const allServices = await DockerService._list(dockerHost); const wantedService = allServices.find((service) => { return service.Spec.Name === networkName; }); return wantedService; } /** * Internal: Create a service * Public API: Use dockerHost.createService(descriptor) instead */ public static async _create( dockerHost: DockerHost, serviceCreationDescriptor: interfaces.IServiceCreationDescriptor, ): Promise { logger.log( 'info', `now creating service ${serviceCreationDescriptor.name}`, ); // 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) { throw new Error(`Image not found: ${serviceCreationDescriptor.image}`); } } else { imageInstance = serviceCreationDescriptor.image; } const serviceVersion = await imageInstance.getVersion(); const labels: interfaces.TLabels = { ...serviceCreationDescriptor.labels, version: serviceVersion, }; const mounts: Array<{ /** * the target inside the container */ Target: string; /** * The Source from which to mount the data (Volume or host path) */ Source: string; Type: 'bind' | 'volume' | 'tmpfs' | 'npipe'; ReadOnly: boolean; Consistency: 'default' | 'consistent' | 'cached' | 'delegated'; }> = []; if (serviceCreationDescriptor.accessHostDockerSock) { mounts.push({ Target: '/var/run/docker.sock', Source: '/var/run/docker.sock', Consistency: 'default', ReadOnly: false, Type: 'bind', }); } if ( serviceCreationDescriptor.resources && serviceCreationDescriptor.resources.volumeMounts ) { for (const volumeMount of serviceCreationDescriptor.resources .volumeMounts) { mounts.push({ Target: volumeMount.containerFsPath, Source: volumeMount.hostFsPath, Consistency: 'default', ReadOnly: false, Type: 'bind', }); } } // Resolve networks (support both string[] and DockerNetwork[]) const networkArray: Array<{ Target: string; Aliases: string[]; }> = []; for (const network of serviceCreationDescriptor.networks) { // Skip null networks (can happen if network creation fails) if (!network) { logger.log('warn', 'Skipping null network in service creation'); continue; } // Resolve network name const networkName = typeof network === 'string' ? network : network.Name; networkArray.push({ Target: networkName, Aliases: [serviceCreationDescriptor.networkAlias], }); } const ports = []; for (const port of serviceCreationDescriptor.ports) { const portArray = port.split(':'); const hostPort = portArray[0]; const containerPort = portArray[1]; ports.push({ Protocol: 'tcp', PublishedPort: parseInt(hostPort, 10), TargetPort: parseInt(containerPort, 10), }); } // Resolve secrets (support both string[] and DockerSecret[]) const secretArray: any[] = []; for (const secret of serviceCreationDescriptor.secrets) { // Resolve secret instance let secretInstance: DockerSecret; if (typeof secret === 'string') { secretInstance = await DockerSecret._fromName(dockerHost, secret); if (!secretInstance) { throw new Error(`Secret not found: ${secret}`); } } else { secretInstance = secret; } secretArray.push({ File: { Name: 'secret.json', // TODO: make sure that works with multiple secrets UID: '33', GID: '33', Mode: 384, }, SecretID: secretInstance.ID, SecretName: secretInstance.Spec.Name, }); } // lets configure limits const memoryLimitMB = serviceCreationDescriptor.resources && serviceCreationDescriptor.resources.memorySizeMB ? 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: { ContainerSpec: { Image: imageInstance.RepoTags[0], Labels: labels, Secrets: secretArray, Mounts: mounts, /* DNSConfig: { Nameservers: ['1.1.1.1'] } */ }, UpdateConfig: { Parallelism: 0, Delay: 0, FailureAction: 'pause', Monitor: 15000000000, MaxFailureRatio: 0.15, }, ForceUpdate: 1, Resources: { Limits: limits, }, LogDriver: { Name: 'json-file', Options: { 'max-file': '3', 'max-size': '10M', }, }, }, Labels: labels, Networks: networkArray, EndpointSpec: { Ports: ports, }, }); const createdService = await DockerService._fromName( dockerHost, serviceCreationDescriptor.name, ); return createdService; } // 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: { Name: string; Labels: interfaces.TLabels; TaskTemplate: { ContainerSpec: { Image: string; Isolation: string; Secrets: Array<{ File: { Name: string; UID: string; GID: string; Mode: number; }; SecretID: string; SecretName: string; }>; }; ForceUpdate: 0; }; Mode: {}; Networks: [any[]]; }; public Endpoint: { Spec: {}; VirtualIPs: [any[]] }; constructor(dockerHostArg: DockerHost) { super(dockerHostArg); } // INSTANCE METHODS /** * Refreshes this service's state from the Docker daemon */ public async refresh(): Promise { const updated = await DockerService._fromName(this.dockerHost, this.Spec.Name); if (updated) { Object.assign(this, updated); } } /** * Removes this service from the Docker daemon */ public async remove() { await this.dockerHost.request('DELETE', `/services/${this.ID}`); } /** * Re-reads service data from Docker engine * @deprecated Use refresh() instead */ public async reReadFromDockerEngine() { const dockerData = await this.dockerHost.request( 'GET', `/services/${this.ID}`, ); // TODO: Better assign: Object.assign(this, dockerData); } /** * Checks if this service needs an update based on image version */ public async needsUpdate(): Promise { // TODO: implement digest based update recognition await this.reReadFromDockerEngine(); const dockerImage = await DockerImage._createFromRegistry( this.dockerHost, { creationObject: { imageUrl: this.Spec.TaskTemplate.ContainerSpec.Image, }, }, ); const imageVersion = new plugins.smartversion.SmartVersion( dockerImage.Labels.version, ); const serviceVersion = new plugins.smartversion.SmartVersion( this.Spec.Labels.version, ); if (imageVersion.greaterThan(serviceVersion)) { console.log(`service ${this.Spec.Name} needs to be updated`); return true; } else { console.log(`service ${this.Spec.Name} is up to date.`); } } }