import yaml from 'js-yaml';
import fs from 'fs';
import deepmerge from 'deepmerge'
/**
* Docker Compose File class.
* Represents the yaml object of a docker-compose file.
* Allows for merging and mapping of compose files.
*/
class DockerComposeFile {
/**
* Creates a DockerComposeFile instance with provided docker-compose.yml file or string.
* @param {String|DockerComposeFile} composeFileString a path to a docker-compose.yml file, a full yaml string or a DockerComposeFile instance.
* @example
* const composeFile = new DockerComposeFile(`${__dirname}/docker-compose.yml`);
* @example
* const composeFile = new DockerComposeFile(`${__dirname}/docker-compose.yml`, `${__dirname}/docker-compose.vpn.yml`);
* @example
* const composeFile = new DockerComposeFile(`${__dirname}/docker-compose.yml, "version: '3'");
* @returns {DockerComposeFile} DockerComposeFile instance with all files or strings merged together.
*/
constructor(...composeFiles) {
this.add(...composeFiles);
}
/**
* Merges the provided compose files into the current yaml class object.
* @param {String|DockerComposeFile} composeFiles Any number of compose file paths or DockerComposeFile objects.
* @returns {String} Merged yaml string.
*/
add(...composeFiles) {
for(let i = 0; i < composeFiles.length; i++) {
let composeFile = composeFiles[i];
if (composeFile instanceof DockerComposeFile) {
composeFile = composeFile.yaml;
} else if (composeFile?.includes('.yml')) {
composeFile = fs.readFileSync(composeFile, 'utf8').trim();
}
this.yamlObject = deepmerge(this.yamlObject, yaml.load(composeFile), {
arrayMerge: (target, source, options) => {
function onlyUnique(value, index, self) {
return self.indexOf(value) === index;
}
return target.concat(source).filter(onlyUnique).map(function(element) {
return options.cloneUnlessOtherwiseSpecified(element, options)
})
}
});
}
return this.yaml;
}
/**
* Merges the new yaml string to the current yaml also takes file paths or DockerComposeFiles.
* @type {String}
*/
set yaml(yamlString) {
return this.add(yamlString);
}
get yaml() {
return yaml.dump(this.yamlObject, {'sortKeys': true}).trim();
}
/**
* Save the yaml file to disk.
* @param path File path to save the yaml.
*/
write(path) {
fs.writeFileSync(path, this.yaml);
}
/**
* Takes a docker-compose file with template strings and generates an array of substituted docker compose files.
* Only operates with arrays of substitutions of the same length.
* @param {Array<String, Array<String>>} substitutions Each set of substitutions an key and an array of values to map.
* @example
* // docker-compose file containing template strings for ${REGION} and ${ID}.
* const yamlString =
* `
* services:
* vpn-${REGION}-${ID}:
* container_name: 'vpn-${REGION}-${ID}'
* environment:
* - REGION=${REGION}
* `;
* const composeFileTemplate = new DockerComposeFile(yamlString);
* // Will return a array of 2 compose files for each substitution.
* const composeFiles = composeFileTemplate.mapTemplate(
* ['REGION', ['US_West', 'US_Seattle']],
* ['ID', ['1', '2']]
* );
* @returns Array of substituted docker compose files.
* @todo allow for variable length substitutions.
*/
mapTemplate(...substitutions) {
let composeTemplate = this.yaml;
let composeFiles = [];
while(substitutions.length) {
let [stringTemplate, subs] = substitutions.pop();
for (let i = 0; i < subs.length; i++) {
if (composeFiles[i] === undefined) {
composeFiles[i] = composeTemplate;
}
composeFiles[i] = composeFiles[i].replaceAll('${' + stringTemplate +'}', subs[i]);
}
}
return composeFiles.map((file) => new DockerComposeFile(file));
}
}
export default DockerComposeFile;