UNPKG

podletjs

Version:

JavaScript port of Podlet - Generate Podman Quadlet files from Docker run commands and compose files

390 lines (337 loc) 11.1 kB
/** * Container class representing a Podman Quadlet container configuration * Based on the Rust quadlet::Container struct */ export class Container { constructor() { // Basic container properties this.image = ''; this.containerName = null; this.exec = null; // Capabilities this.addCapability = []; this.dropCapability = []; // Devices and mounts this.addDevice = []; this.mount = []; this.volume = []; // Network configuration this.network = []; this.networkAlias = []; this.publishPort = []; this.exposeHostPort = []; this.ip = null; this.ip6 = null; // DNS configuration this.dns = []; this.dnsOption = []; this.dnsSearch = []; // Environment this.environment = []; this.environmentFile = []; this.environmentHost = false; // Security this.noNewPrivileges = false; this.securityLabelDisable = false; this.securityLabelFileType = null; this.securityLabelLevel = null; this.securityLabelNested = false; this.securityLabelType = null; this.seccompProfile = null; this.mask = []; this.unmask = null; // User and group this.user = null; this.group = null; this.groupAdd = []; this.userNS = null; this.uidMap = []; this.gidMap = []; this.subUidMap = null; this.subGidMap = null; // Runtime options this.readOnly = false; this.readOnlyTmpfs = true; this.runInit = false; this.workingDir = null; this.hostName = null; this.timezone = null; this.shmSize = null; this.tmpfs = []; // Health checks this.healthCmd = null; this.healthInterval = null; this.healthOnFailure = null; this.healthRetries = null; this.healthStartPeriod = null; this.healthStartupCmd = null; this.healthStartupInterval = null; this.healthStartupRetries = null; this.healthStartupSuccess = null; this.healthStartupTimeout = null; this.healthTimeout = null; // Systemd integration this.notify = 'conmon'; this.stopSignal = null; this.stopTimeout = null; // Pod integration this.pod = null; // Logging this.logDriver = null; this.logOpt = []; // Labels and annotations this.label = []; this.annotation = []; // Resource limits this.pidsLimit = null; this.ulimit = []; this.sysctl = []; // Auto update this.autoUpdate = null; // Pull policy this.pull = null; // Secrets this.secret = []; // Rootfs this.rootfs = null; // Entrypoint this.entrypoint = null; // Additional Podman arguments this.podmanArgs = null; } /** * Set the container image with validation */ setImage(image) { if (!image || typeof image !== 'string' || image.trim() === '') { throw new Error('Image must be a non-empty string'); } this.image = image.trim(); return this; } /** * Set the container name with validation */ setContainerName(name) { if (!name || typeof name !== 'string' || name.trim() === '') { throw new Error('Container name must be a non-empty string'); } // Validate container name format (similar to Docker/Podman rules) const namePattern = /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/; const trimmedName = name.trim(); if (!namePattern.test(trimmedName)) { throw new Error('Container name must start with alphanumeric character and contain only alphanumeric characters, underscores, periods, and hyphens'); } this.containerName = trimmedName; return this; } /** * Set the exec command */ setExec(command) { if (command && typeof command !== 'string') { throw new Error('Exec command must be a string'); } this.exec = command; return this; } /** * Add a published port with validation */ addPublishPort(port) { if (!port || typeof port !== 'string' || port.trim() === '') { throw new Error('Port must be a non-empty string'); } const trimmedPort = port.trim(); // Validate port format (host:container or just container port, optionally with protocol) const portPattern = /^(\d+:)?\d+(\/(?:tcp|udp|sctp))?$/; if (!portPattern.test(trimmedPort)) { throw new Error('Port must be in format "host:container" or "container", optionally with protocol "/tcp", "/udp", or "/sctp"'); } // Check port ranges (1-65535) // Remove protocol suffix for validation const portForValidation = trimmedPort.replace(/\/(?:tcp|udp|sctp)$/, ''); const ports = portForValidation.split(':'); for (const p of ports) { const portNum = parseInt(p, 10); if (portNum < 1 || portNum > 65535) { throw new Error(`Port ${portNum} is out of valid range (1-65535)`); } } this.publishPort.push(trimmedPort); return this; } /** * Add an environment variable with validation */ addEnvironment(env) { if (!env || typeof env !== 'string' || env.trim() === '') { throw new Error('Environment variable must be a non-empty string'); } const trimmedEnv = env.trim(); // Validate environment variable format (KEY=value) if (!trimmedEnv.includes('=')) { throw new Error('Environment variable must be in format "KEY=value"'); } this.environment.push(trimmedEnv); return this; } /** * Add a volume mount with validation */ addVolume(volume) { if (!volume || typeof volume !== 'string' || volume.trim() === '') { throw new Error('Volume must be a non-empty string'); } const trimmedVolume = volume.trim(); // Validate volume format (source:destination or just destination) if (!trimmedVolume.includes(':') && !trimmedVolume.startsWith('/')) { throw new Error('Volume must be in format "source:destination" or an absolute path'); } this.volume.push(trimmedVolume); return this; } /** * Add a label with validation */ addLabel(label) { if (!label || typeof label !== 'string' || label.trim() === '') { throw new Error('Label must be a non-empty string'); } const trimmedLabel = label.trim(); // Validate label format (key=value) if (!trimmedLabel.includes('=')) { throw new Error('Label must be in format "key=value"'); } this.label.push(trimmedLabel); return this; } /** * Set the pod reference */ setPod(pod) { this.pod = pod; return this; } /** * Generate the default container name from the image */ getDefaultName() { if (this.containerName) { return this.containerName; } // Extract name from image (similar to Rust image_to_name function) const imageParts = this.image.split('/'); const imageName = imageParts[imageParts.length - 1]; // Remove tag if present const nameWithoutTag = imageName.split(':')[0]; return nameWithoutTag; } /** * Validate the container configuration */ validate() { if (!this.image) { throw new Error('Image is required'); } // Validate ports for (const port of this.publishPort) { try { const ports = port.split(':'); for (const p of ports) { const portNum = parseInt(p, 10); if (isNaN(portNum) || portNum < 1 || portNum > 65535) { throw new Error(`Invalid port: ${port}`); } } } catch (e) { if (e.message.startsWith('Invalid port:')) { throw e; } throw new Error(`Invalid port format: ${port}`); } } } /** * Create a deep copy of the container */ clone() { const cloned = new Container(); // Copy primitive properties cloned.image = this.image; cloned.containerName = this.containerName; cloned.exec = this.exec; cloned.ip = this.ip; cloned.ip6 = this.ip6; cloned.environmentHost = this.environmentHost; cloned.noNewPrivileges = this.noNewPrivileges; cloned.securityLabelDisable = this.securityLabelDisable; cloned.securityLabelFileType = this.securityLabelFileType; cloned.securityLabelLevel = this.securityLabelLevel; cloned.securityLabelNested = this.securityLabelNested; cloned.securityLabelType = this.securityLabelType; cloned.seccompProfile = this.seccompProfile; cloned.unmask = this.unmask; cloned.user = this.user; cloned.group = this.group; cloned.userNS = this.userNS; cloned.subUidMap = this.subUidMap; cloned.subGidMap = this.subGidMap; cloned.readOnly = this.readOnly; cloned.readOnlyTmpfs = this.readOnlyTmpfs; cloned.runInit = this.runInit; cloned.workingDir = this.workingDir; cloned.hostName = this.hostName; cloned.timezone = this.timezone; cloned.shmSize = this.shmSize; cloned.healthCmd = this.healthCmd; cloned.healthInterval = this.healthInterval; cloned.healthOnFailure = this.healthOnFailure; cloned.healthRetries = this.healthRetries; cloned.healthStartPeriod = this.healthStartPeriod; cloned.healthTimeout = this.healthTimeout; cloned.healthStartupCmd = this.healthStartupCmd; cloned.healthStartupInterval = this.healthStartupInterval; cloned.healthStartupRetries = this.healthStartupRetries; cloned.healthStartupSuccess = this.healthStartupSuccess; cloned.healthStartupTimeout = this.healthStartupTimeout; cloned.notify = this.notify; cloned.stopSignal = this.stopSignal; cloned.stopTimeout = this.stopTimeout; cloned.pod = this.pod; cloned.logDriver = this.logDriver; cloned.pidsLimit = this.pidsLimit; cloned.autoUpdate = this.autoUpdate; cloned.pull = this.pull; cloned.rootfs = this.rootfs; cloned.entrypoint = this.entrypoint; cloned.podmanArgs = this.podmanArgs; // Copy array properties (deep copy) cloned.addCapability = [...this.addCapability]; cloned.dropCapability = [...this.dropCapability]; cloned.addDevice = [...this.addDevice]; cloned.mount = [...this.mount]; cloned.volume = [...this.volume]; cloned.network = [...this.network]; cloned.networkAlias = [...this.networkAlias]; cloned.publishPort = [...this.publishPort]; cloned.exposeHostPort = [...this.exposeHostPort]; cloned.dns = [...this.dns]; cloned.dnsOption = [...this.dnsOption]; cloned.dnsSearch = [...this.dnsSearch]; cloned.environment = [...this.environment]; cloned.environmentFile = [...this.environmentFile]; cloned.mask = [...this.mask]; cloned.groupAdd = [...this.groupAdd]; cloned.uidMap = [...this.uidMap]; cloned.gidMap = [...this.gidMap]; cloned.tmpfs = [...this.tmpfs]; cloned.label = [...this.label]; cloned.logOpt = [...this.logOpt]; cloned.annotation = [...this.annotation]; cloned.ulimit = [...this.ulimit]; cloned.sysctl = [...this.sysctl]; cloned.secret = [...this.secret]; return cloned; } }