UNPKG

@puls-atlas/cli

Version:

The Puls Atlas CLI tool for managing Atlas projects

184 lines 9.87 kB
import fs from 'fs'; import os from 'os'; import path from 'path'; import { execFileSync } from 'child_process'; const SEARCH_TERRAFORM_ROOT_TEMPLATE_FILES = ['main.tf.tpl', 'variables.tf.tpl']; const SEARCH_TERRAFORM_ROOT_TEMPLATE_DIRECTORY = path.join('templates', 'search-root'); const SEARCH_PROVIDER_TERRAFORM_ROOT_TEMPLATE_DIRECTORY = path.join('templates', 'search-provider-root'); const SEARCH_PROVIDER_GKE_TERRAFORM_ROOT_TEMPLATE_DIRECTORY = path.join('templates', 'search-provider-gke-root'); const SEARCH_PROVIDER_PLATFORM_TERRAFORM_ROOT_TEMPLATE_DIRECTORY = path.join('templates', 'search-provider-platform-root'); const SEARCH_RUNTIME_TERRAFORM_ROOT_TEMPLATE_DIRECTORY = path.join('templates', 'search-runtime-root'); const GITHUB_TERRAFORM_MODULE_SOURCE_PATTERN = /^git::https:\/\/github\.com\/(?<owner>[^/]+)\/(?<repo>[^/.?]+?)(?:\.git)?\/\/(?<modulePath>[^?]+?)(?:\?(?<query>.*))?$/; const normalizeTemplateFileName = templateFileName => templateFileName.replace(/\.tpl$/u, ''); const hasTemplateDirectory = (templateDirectory, templateFiles = SEARCH_TERRAFORM_ROOT_TEMPLATE_FILES, existsSyncImpl = fs.existsSync) => templateFiles.every(templateFileName => existsSyncImpl(path.join(templateDirectory, templateFileName))); const readTemplateDirectory = (templateDirectory, templateFiles = SEARCH_TERRAFORM_ROOT_TEMPLATE_FILES, readFileSyncImpl = fs.readFileSync) => Object.fromEntries(templateFiles.map(templateFileName => [normalizeTemplateFileName(templateFileName), readFileSyncImpl(path.join(templateDirectory, templateFileName), 'utf-8')])); const walkParentDirectories = startPath => { const directories = []; let currentPath = startPath; while (true) { directories.push(currentPath); const parentPath = path.dirname(currentPath); if (parentPath === currentPath) { return directories; } currentPath = parentPath; } }; 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 provider module source.'); }; const resolveLocalTemplateDirectoryFromModuleSource = (moduleSource, rootPath, templateRootDirectory, options = {}) => { const { existsSyncImpl = fs.existsSync, readdirSyncImpl = fs.readdirSync, templateFiles = SEARCH_TERRAFORM_ROOT_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; } } } const gitHubSource = parseGitHubTerraformModuleSource(moduleSource); if (!gitHubSource) { return null; } for (const candidatePath of walkParentDirectories(rootPath)) { const templateDirectory = path.join(candidatePath, gitHubSource.repo, templateRootDirectory); if (hasTemplateDirectory(templateDirectory, templateFiles, existsSyncImpl)) { return templateDirectory; } let nestedDirectories = []; try { nestedDirectories = readdirSyncImpl(candidatePath, { withFileTypes: true }).filter(entry => entry.isDirectory()).map(entry => entry.name); } catch { nestedDirectories = []; } for (const nestedDirectory of nestedDirectories) { const nestedTemplateDirectory = path.join(candidatePath, nestedDirectory, gitHubSource.repo, templateRootDirectory); if (hasTemplateDirectory(nestedTemplateDirectory, templateFiles, existsSyncImpl)) { return nestedTemplateDirectory; } } } return null; }; const resolveTaggedTemplateCloneUrl = moduleSource => { const gitHubSource = parseGitHubTerraformModuleSource(moduleSource); if (!gitHubSource) { return null; } return `https://github.com/${gitHubSource.owner}/${gitHubSource.repo}.git`; }; const loadTaggedTemplateFiles = async (moduleSource, templateRootDirectory, options = {}) => { const { execFileSyncImpl = execFileSync, mkdtempSyncImpl = fs.mkdtempSync, readFileSyncImpl = fs.readFileSync, rmSyncImpl = fs.rmSync, templateFiles = SEARCH_TERRAFORM_ROOT_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-search-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 readTemplateDirectory(templateDirectory, templateFiles, readFileSyncImpl); } finally { rmSyncImpl(cloneDirectory, { force: true, recursive: true }); } }; export const loadTaggedSearchTerraformRootFiles = async (moduleSource, options = {}) => loadTaggedTemplateFiles(moduleSource, SEARCH_TERRAFORM_ROOT_TEMPLATE_DIRECTORY, options); export const loadTaggedSearchProviderTerraformRootFiles = async (moduleSource, options = {}) => loadTaggedTemplateFiles(moduleSource, SEARCH_PROVIDER_TERRAFORM_ROOT_TEMPLATE_DIRECTORY, options); export const loadTaggedSearchProviderPlatformTerraformRootFiles = async (moduleSource, options = {}) => loadTaggedTemplateFiles(moduleSource, SEARCH_PROVIDER_PLATFORM_TERRAFORM_ROOT_TEMPLATE_DIRECTORY, options); const loadTemplateFiles = async (moduleSource, rootPath, templateRootDirectory, options = {}) => { const { existsSyncImpl = fs.existsSync, readFileSyncImpl = fs.readFileSync, templateFiles = SEARCH_TERRAFORM_ROOT_TEMPLATE_FILES, templateDirectory } = options; const resolvedTemplateDirectory = templateDirectory ?? resolveLocalTemplateDirectoryFromModuleSource(moduleSource, rootPath, templateRootDirectory, { existsSyncImpl, templateFiles }); if (resolvedTemplateDirectory) { return readTemplateDirectory(resolvedTemplateDirectory, templateFiles, readFileSyncImpl); } return loadTaggedTemplateFiles(moduleSource, templateRootDirectory, options); }; export const loadTerraformRootTemplateFiles = async (moduleSource, rootPath, templateRootDirectory, options = {}) => loadTemplateFiles(moduleSource, rootPath, templateRootDirectory, options); export const loadSearchTerraformRootTemplateFiles = async (moduleSource, rootPath, options = {}) => loadTemplateFiles(moduleSource, rootPath, SEARCH_TERRAFORM_ROOT_TEMPLATE_DIRECTORY, options); export const loadSearchTerraformRuntimeRootTemplateFiles = async (moduleSource, rootPath, options = {}) => { try { return await loadTemplateFiles(moduleSource, rootPath, SEARCH_RUNTIME_TERRAFORM_ROOT_TEMPLATE_DIRECTORY, options); } catch { return loadTemplateFiles(moduleSource, rootPath, SEARCH_TERRAFORM_ROOT_TEMPLATE_DIRECTORY, options); } }; export const loadSearchProviderTerraformRootTemplateFiles = async (moduleSource, rootPath, options = {}) => loadTemplateFiles(moduleSource, rootPath, SEARCH_PROVIDER_TERRAFORM_ROOT_TEMPLATE_DIRECTORY, options); export const loadSearchProviderGkeTerraformRootTemplateFiles = async (moduleSource, rootPath, options = {}) => loadTemplateFiles(moduleSource, rootPath, SEARCH_PROVIDER_GKE_TERRAFORM_ROOT_TEMPLATE_DIRECTORY, options); export const loadSearchProviderPlatformTerraformRootTemplateFiles = async (moduleSource, rootPath, options = {}) => loadTemplateFiles(moduleSource, rootPath, SEARCH_PROVIDER_PLATFORM_TERRAFORM_ROOT_TEMPLATE_DIRECTORY, options); export const renderSearchTerraformRootTemplateFiles = (templateFiles, replacements) => Object.fromEntries(Object.entries(templateFiles).map(([fileName, content]) => { let renderedContent = content; for (const [placeholder, value] of Object.entries(replacements)) { renderedContent = renderedContent.replaceAll(placeholder, String(value)); } return [fileName, renderedContent]; })); export default { loadSearchProviderPlatformTerraformRootTemplateFiles, loadSearchProviderTerraformRootTemplateFiles, loadSearchTerraformRootTemplateFiles, loadTaggedSearchProviderPlatformTerraformRootFiles, loadTaggedSearchProviderTerraformRootFiles, loadTaggedSearchTerraformRootFiles, parseGitHubTerraformModuleSource, resolveSiblingTerraformModuleSource, renderSearchTerraformRootTemplateFiles };