@puls-atlas/cli
Version:
The Puls Atlas CLI tool for managing Atlas projects
130 lines • 6.18 kB
JavaScript
import { execFileSync } from 'child_process';
import fs from 'fs';
import os from 'os';
import path from 'path';
const DEFAULT_TEMPLATE_FILES = ['main.tf.tpl', 'variables.tf.tpl'];
const GITHUB_TERRAFORM_MODULE_SOURCE_PATTERN = /^git::https:\/\/github\.com\/(?<owner>[^/]+)\/(?<repo>[^/.?]+?)(?:\.git)?\/\/(?<modulePath>[^?]+?)(?:\?(?<query>.*))?$/;
const walkParentDirectories = startingPath => {
const directories = [];
let currentPath = path.resolve(startingPath);
while (true) {
directories.push(currentPath);
const parentPath = path.dirname(currentPath);
if (parentPath === currentPath) {
return directories;
}
currentPath = parentPath;
}
};
const hasTemplateDirectory = (templateDirectory, templateFiles = DEFAULT_TEMPLATE_FILES, existsSyncImpl = fs.existsSync) => templateFiles.every(fileName => existsSyncImpl(path.join(templateDirectory, fileName)));
const resolveTaggedTemplateCloneUrl = moduleSource => {
const parsedModuleSource = parseGitHubTerraformModuleSource(moduleSource);
if (!parsedModuleSource) {
return null;
}
return `https://github.com/${parsedModuleSource.owner}/${parsedModuleSource.repo}.git`;
};
export const parseGitHubTerraformModuleSource = moduleSource => {
if (typeof moduleSource !== 'string') {
return null;
}
const match = moduleSource.match(GITHUB_TERRAFORM_MODULE_SOURCE_PATTERN);
if (!match?.groups) {
return null;
}
const query = new URLSearchParams(match.groups.query ?? '');
const ref = query.get('ref');
if (!ref) {
return null;
}
return {
modulePath: match.groups.modulePath,
owner: match.groups.owner,
ref,
repo: match.groups.repo
};
};
export const resolveSiblingTerraformModuleSource = (moduleSource, siblingModulePath) => {
const gitHubSource = parseGitHubTerraformModuleSource(moduleSource);
if (gitHubSource) {
return `git::https://github.com/${gitHubSource.owner}/${gitHubSource.repo}.git//${siblingModulePath}?ref=${gitHubSource.ref}`;
}
if (typeof moduleSource === 'string' && !moduleSource.includes('::')) {
return path.posix.join(path.posix.dirname(moduleSource.replaceAll('\\', '/')), siblingModulePath);
}
throw new Error('Could not resolve a sibling Terraform module source from the configured module source.');
};
const resolveLocalTemplateDirectoryFromModuleSource = (moduleSource, rootPath, templateRootDirectory, options = {}) => {
const {
existsSyncImpl = fs.existsSync,
templateFiles = DEFAULT_TEMPLATE_FILES
} = options;
if (typeof moduleSource === 'string' && !moduleSource.includes('::')) {
const absoluteModulePath = path.resolve(rootPath, moduleSource);
for (const candidatePath of walkParentDirectories(absoluteModulePath)) {
const templateDirectory = path.join(candidatePath, templateRootDirectory);
if (hasTemplateDirectory(templateDirectory, templateFiles, existsSyncImpl)) {
return templateDirectory;
}
}
}
if (!path.isAbsolute(rootPath)) {
return null;
}
for (const candidatePath of walkParentDirectories(rootPath)) {
const templateDirectory = path.join(candidatePath, templateRootDirectory);
if (hasTemplateDirectory(templateDirectory, templateFiles, existsSyncImpl)) {
return templateDirectory;
}
}
return null;
};
const loadTaggedTemplateFiles = async (moduleSource, templateRootDirectory, options = {}) => {
const {
execFileSyncImpl = execFileSync,
mkdtempSyncImpl = fs.mkdtempSync,
readFileSyncImpl = fs.readFileSync,
rmSyncImpl = fs.rmSync,
templateFiles = DEFAULT_TEMPLATE_FILES,
tmpdirImpl = os.tmpdir
} = options;
const gitHubSource = parseGitHubTerraformModuleSource(moduleSource);
const cloneUrl = resolveTaggedTemplateCloneUrl(moduleSource);
if (!gitHubSource || !cloneUrl) {
throw new Error('Tagged Terraform root templates require a git::https://github.com/... module source with a ref query parameter.');
}
const cloneDirectory = mkdtempSyncImpl(path.join(tmpdirImpl(), 'atlas-terraform-templates-'));
try {
execFileSyncImpl('git', ['clone', '--depth', '1', '--branch', gitHubSource.ref, cloneUrl, cloneDirectory], {
stdio: 'pipe'
});
const templateDirectory = path.join(cloneDirectory, templateRootDirectory);
if (!hasTemplateDirectory(templateDirectory, templateFiles, fs.existsSync)) {
throw new Error(`Could not find Terraform root templates under ${templateRootDirectory} in ${cloneUrl} at ref ${gitHubSource.ref}.`);
}
return Object.fromEntries(templateFiles.map(fileName => [fileName, readFileSyncImpl(path.join(templateDirectory, fileName), 'utf-8')]));
} finally {
rmSyncImpl(cloneDirectory, {
force: true,
recursive: true
});
}
};
const loadTemplateFiles = async (moduleSource, rootPath, templateRootDirectory, options = {}) => {
const {
existsSyncImpl = fs.existsSync,
readFileSyncImpl = fs.readFileSync,
templateDirectory,
templateFiles = DEFAULT_TEMPLATE_FILES
} = options;
const resolvedTemplateDirectory = templateDirectory ?? resolveLocalTemplateDirectoryFromModuleSource(moduleSource, rootPath, templateRootDirectory, options);
if (resolvedTemplateDirectory) {
if (!hasTemplateDirectory(resolvedTemplateDirectory, templateFiles, existsSyncImpl)) {
throw new Error(`Could not find Terraform root templates under ${resolvedTemplateDirectory}.`);
}
return Object.fromEntries(templateFiles.map(fileName => [fileName, readFileSyncImpl(path.join(resolvedTemplateDirectory, fileName), 'utf-8')]));
}
return loadTaggedTemplateFiles(moduleSource, templateRootDirectory, options);
};
export const loadTerraformRootTemplateFiles = async (moduleSource, rootPath, templateRootDirectory, options = {}) => loadTemplateFiles(moduleSource, rootPath, templateRootDirectory, options);
export const renderTerraformRootTemplateFiles = (templateFiles, replacements) => Object.fromEntries(Object.entries(templateFiles).map(([fileName, templateContent]) => [fileName.replace(/\.tpl$/u, ''), Object.entries(replacements).reduce((content, [placeholder, replacement]) => content.replaceAll(placeholder, String(replacement)), templateContent)]));