import { existsSync, readFileSync } from 'node:fs'; import JSON5 from 'json5'; import { PluginBase } from '@iobroker/plugin-base'; import DockerManagerOfOwnContainers from './lib/DockerManagerOfOwnContainers'; import DockerManager from './lib/DockerManager'; import type { ContainerConfig, ContainerInfo, ImageInfo, NetworkDriver, NetworkInfo, VolumeDriver, DockerContainerInspect, DockerImageTagsResponse, VolumeInfo, ImageName, DiskUsage, DockerImageInspect, PortBinding, Protocol, Security, VolumeMount, LsEntry, } from './types'; import composeFromYaml, { type ComposeTop } from './lib/parseDockerCompose'; import { composeToContainerConfigs } from './lib/compose2config'; import { walkTheConfig } from './lib/templates'; export type DockerConfig = ComposeTop; export { DockerManagerOfOwnContainers, DockerManager, type ContainerConfig, type ContainerInfo, type ImageInfo, type NetworkDriver, type NetworkInfo, type VolumeDriver, type VolumeInfo, type DockerImageTagsResponse, type DockerContainerInspect, type ImageName, type DiskUsage, type DockerImageInspect, type PortBinding, type Protocol, type Security, type VolumeMount, type LsEntry, }; export default class DockerPlugin extends PluginBase { #dockerManager: DockerManagerOfOwnContainers | null = null; #configurations: ContainerConfig[] = []; #iobDockerApi: | { host: string; port: number; protocol: 'http' | 'https'; ca?: string; cert?: string; key?: string; } | undefined; // ioBroker setting; /** Return the Docker configurations that will be managed */ get configurations(): ContainerConfig[] { return this.#configurations; } /** * Register and initialize Docker * * @param pluginConfig plugin configuration from config files */ async init(pluginConfig: DockerConfig): Promise { // Read the instance config const instanceObj: ioBroker.InstanceObject | null | undefined = (await this.getObject( this.settings.parentNamespace, )) as ioBroker.InstanceObject | null | undefined; if (!instanceObj) { throw new Error(`Cannot find instance object ${this.settings.parentNamespace}`); } if (!this.settings.adapterDir) { // somehow, the adapter used an old plugin-base version. Try to find the adapter directory const adapterName = this.settings.parentPackage.name; if (existsSync(`${__dirname}/../../${adapterName}`)) { this.settings.adapterDir = `${__dirname}/../../${adapterName}`; } else if (existsSync(`${__dirname}/../../../${adapterName}`)) { this.settings.adapterDir = `${__dirname}/../../../${adapterName}`; } else if (existsSync(`${__dirname}/../../../../${adapterName}`)) { this.settings.adapterDir = `${__dirname}/../../../../${adapterName}`; } else if (existsSync(`${__dirname}/../../../../../${adapterName}`)) { this.settings.adapterDir = `${__dirname}/../../../../../${adapterName}`; } else if (existsSync(`${__dirname}/../../../../../../${adapterName}`)) { this.settings.adapterDir = `${__dirname}/../../../../../../${adapterName}`; } else { throw new Error('Cannot find adapter directory, please update plugin-base package'); } } const instance = parseInt(this.settings.parentNamespace.split('.').pop()!, 10); if (isNaN(instance)) { throw new Error(`Cannot find instance number in ${this.settings.parentNamespace}`); } walkTheConfig(pluginConfig, instanceObj.native, { instance, }); // If dockerFiles is specified, read the files and merge them with dockerConfigs if (pluginConfig.iobDockerComposeFiles) { for (const filePath of pluginConfig.iobDockerComposeFiles) { try { const fileContent = readFileSync(`${this.settings.adapterDir}/${filePath}`, 'utf-8'); let parsed: Record | undefined; if (filePath.endsWith('.json')) { try { parsed = JSON.parse(fileContent); } catch (err) { this.log.error(`Cannot parse docker config file ${filePath}: ${err}`); } } else if (filePath.endsWith('.json5')) { try { parsed = JSON5.parse(fileContent); } catch (err) { this.log.error(`Cannot parse docker config file ${filePath}: ${err}`); } } else if (filePath.endsWith('.yaml') || filePath.endsWith('.yml')) { try { parsed = composeFromYaml(fileContent); } catch (err) { this.log.error(`Cannot parse docker config file ${filePath}: ${err}`); } } else { this.log.warn(`Unknown file extension of docker config file ${filePath}`); } if (parsed) { const pureFileConfig: ComposeTop = walkTheConfig(parsed, instanceObj.native, { instance, }); this.log.debug( `Rendered docker-compose file for docker ${filePath}: ${JSON.stringify(pureFileConfig)}`, ); const configs = composeToContainerConfigs(pureFileConfig); for (const config of configs) { if (config.iobEnabled) { this.log.debug( `Use following config for docker ${filePath}: ${JSON.stringify(config)}`, ); this.#configurations.push(config); } } } } catch (err) { this.log.error(`Cannot read docker config file ${filePath}: ${err}`); } } } if (!this.#configurations.length) { this.log.debug('No Docker containers to manage'); return; } // If any container has iobDockerApi, we need to read available docker configurations if (pluginConfig.iobDockerApi && typeof pluginConfig.iobDockerApi === 'string') { // If iobDockerApi is a string, we need to read the system.docker object const systemDockerObj: ioBroker.Object | null | undefined = await this.getObject('system.docker'); const nativeConfig = systemDockerObj?.native as | { hosts: { [name: string]: { host: string; port: number; protocol: 'http' | 'https'; ca?: string; cert?: string; key?: string; }; }; } | undefined; if (nativeConfig) { // Replace all iobDockerApi strings with actual config objects // fallback to the local socket if no system.docker object if (!nativeConfig.hosts[pluginConfig.iobDockerApi]) { this.log.warn(`Cannot find docker configuration for ${pluginConfig.iobDockerApi}`); delete pluginConfig.iobDockerApi; } else { pluginConfig.iobDockerApi = nativeConfig.hosts[pluginConfig.iobDockerApi]; } } else { this.log.warn( 'Cannot find system.docker object, but at least one container requires it. Will use local socket', ); delete pluginConfig.iobDockerApi; } } if (typeof pluginConfig.iobDockerApi === 'object') { this.#iobDockerApi = pluginConfig.iobDockerApi; } if (!this.#configurations.find(conf => conf.iobWaitForReady)) { await this.#startDockerManager(); } } async #startDockerManager(forceRestartOfExistingContainer?: boolean): Promise { if (this.#configurations.find(conf => conf.iobEnabled !== false)) { this.#dockerManager ||= new DockerManagerOfOwnContainers( { dockerApi: this.#iobDockerApi, logger: { level: 'silly', silly: this.log.silly.bind(this.log), debug: this.log.debug.bind(this.log), info: this.log.info.bind(this.log), warn: this.log.warn.bind(this.log), error: this.log.error.bind(this.log), }, adapterDir: this.settings.adapterDir, namespace: this.parentNamespace.replace('system.adapter.', '') as `${string}.${number}`, forceRestart: forceRestartOfExistingContainer, }, this.#configurations, ); await this.#dockerManager.isReady(); } } /** * Return the DockerManager object. This can be used to monitor containers */ getDockerManager(): DockerManagerOfOwnContainers | null { return this.#dockerManager; } /** * This function will be called when the instance prepared all data to be copied to volume. * It should be only called if any container has a flag "iobWaitForReady" * * @returns Promise which will be resolved when all containers are ready (or immediate if no container has the flag */ async instanceIsReady(forceRestartOfExistingContainer?: boolean): Promise { await this.#startDockerManager(forceRestartOfExistingContainer); } }