import type { DockerHost } from './classes.host.js'; import type { DockerService } from './classes.service.js'; import type { INetworkCreationDescriptor, INetworkListOptions, TNetworkRemoveStatus, } from './interfaces/network.js'; import type { TLabels } from './interfaces/label.js'; import { DockerResource } from './classes.base.js'; import { assertDockerNetworkId, assertDockerResponseStatus, assertDockerResponseStatusOneOf, assertNonemptyDockerString, buildDockerFilterRoute, formatDockerResponseBody, rollbackFailedDockerCreation, } from './helpers.docker.js'; interface INetworkContainerAttachment { Name: string; EndpointID: string; MacAddress: string; IPv4Address: string; IPv6Address: string; } export class DockerNetwork extends DockerResource { public static async _list( dockerHostArg: DockerHost, optionsArg: INetworkListOptions = {}, ): Promise { const route = buildDockerFilterRoute('/networks', optionsArg.filters); const response = await dockerHostArg.request('GET', route); assertDockerResponseStatus('Docker network list', 200, response); if (!Array.isArray(response.body)) { throw new Error( `Docker network list failed: expected response body to be an array; response body: ${formatDockerResponseBody(response.body)}`, ); } return response.body.map( (networkObjectArg: Record) => new DockerNetwork(dockerHostArg, networkObjectArg), ); } public static async _fromId( dockerHostArg: DockerHost, idArg: string, ): Promise { assertDockerNetworkId(idArg); const response = await dockerHostArg.request( 'GET', `/networks/${encodeURIComponent(idArg)}`, ); if (response.statusCode === 404) { return undefined; } assertDockerResponseStatus(`Docker network inspect "${idArg}"`, 200, response); const network = new DockerNetwork(dockerHostArg, response.body); if (network.Id !== idArg) { throw new Error( `Docker network inspect "${idArg}" failed: inspected Id "${network.Id}" does not equal the requested immutable ID`, ); } return network; } public static async _fromName( dockerHostArg: DockerHost, nameArg: string, ): Promise { assertNonemptyDockerString(nameArg, 'Docker network name'); const networks = await DockerNetwork._list(dockerHostArg, { filters: { name: [nameArg] }, }); return networks.find((networkArg) => networkArg.Name === nameArg); } public static async _create( dockerHostArg: DockerHost, descriptorArg: INetworkCreationDescriptor, ): Promise { assertNonemptyDockerString(descriptorArg.Name, 'Docker network name'); const response = await dockerHostArg.request('POST', '/networks/create', { Name: descriptorArg.Name, CheckDuplicate: true, Driver: descriptorArg.Driver || 'bridge', EnableIPv6: descriptorArg.EnableIPv6 ?? false, IPAM: descriptorArg.IPAM, Internal: descriptorArg.Internal ?? false, Attachable: descriptorArg.Attachable ?? false, Labels: descriptorArg.Labels || {}, Options: descriptorArg.Options || {}, Ingress: false, }); assertDockerResponseStatus('Docker network create', 201, response); const createdId = response.body?.Id; try { assertDockerNetworkId(createdId, 'Docker network create response body.Id'); } catch (error) { const verificationError = new Error( `Docker network create failed: ${error instanceof Error ? error.message : formatDockerResponseBody(error)}; response body: ${formatDockerResponseBody(response.body)}`, ); return await rollbackFailedDockerCreation( `Docker network "${descriptorArg.Name}"`, verificationError, async () => { const rollbackResponse = await dockerHostArg.request( 'DELETE', `/networks/${encodeURIComponent(descriptorArg.Name)}`, ); assertDockerResponseStatusOneOf( `Docker network rollback remove "${descriptorArg.Name}"`, [204, 404], rollbackResponse, ); }, ); } try { const createdNetwork = await DockerNetwork._fromId(dockerHostArg, createdId); if (!createdNetwork) { throw new Error( `Docker network create failed: direct inspect did not find created ID "${createdId}"`, ); } return createdNetwork; } catch (verificationError) { return await rollbackFailedDockerCreation( `Docker network "${createdId}"`, verificationError, async () => { const rollbackResponse = await dockerHostArg.request( 'DELETE', `/networks/${encodeURIComponent(createdId)}`, ); assertDockerResponseStatusOneOf( `Docker network rollback remove "${createdId}"`, [204, 404], rollbackResponse, ); }, ); } } 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!: boolean; public Labels!: TLabels; public Options!: Record; public Containers?: Record; public IPAM!: { Driver: string; Config: Array<{ Subnet?: string; IPRange?: string; Gateway?: string; }>; Options?: Record; }; private readonly immutableId: string; constructor( dockerHostArg: DockerHost, dockerNetworkObjectArg: Record, ) { super(dockerHostArg); assertDockerNetworkId( dockerNetworkObjectArg.Id, 'Docker network response Id', ); this.immutableId = dockerNetworkObjectArg.Id; Object.assign(this, dockerNetworkObjectArg); } public async refresh(): Promise { const updatedNetwork = await DockerNetwork._fromId( this.dockerHost, this.immutableId, ); if (!updatedNetwork) { throw new Error( `Docker network refresh failed: network "${this.immutableId}" was not found`, ); } Object.assign(this, updatedNetwork); this.Id = this.immutableId; } public hasLabels(requiredLabelsArg: TLabels): boolean { return Object.entries(requiredLabelsArg).every( ([keyArg, valueArg]) => this.Labels?.[keyArg] === valueArg, ); } public async remove(): Promise { const response = await this.dockerHost.request( 'DELETE', `/networks/${encodeURIComponent(this.immutableId)}`, ); assertDockerResponseStatusOneOf( `Docker network remove "${this.immutableId}"`, [204, 404], response, ); return response.statusCode === 204 ? 'removed' : 'already-removed'; } public async listContainersOnNetwork(): Promise { await this.refresh(); return Object.values(this.Containers || {}); } public async getContainersOnNetworkForService( serviceArg: DockerService, ): Promise { const containersOnNetwork = await this.listContainersOnNetwork(); return containersOnNetwork.filter((containerArg) => containerArg.Name === serviceArg.Spec.Name || containerArg.Name.startsWith(`${serviceArg.Spec.Name}.`), ); } }