UNPKG

@puls-atlas/cli

Version:

The Puls Atlas CLI tool for managing Atlas projects

130 lines 6.18 kB
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)]));