331 lines
9.0 KiB
TypeScript
331 lines
9.0 KiB
TypeScript
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<DockerService> {
|
|
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<DockerService> {
|
|
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<void> {
|
|
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<boolean> {
|
|
// 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.`);
|
|
}
|
|
}
|
|
}
|