@puls-atlas/cli
Version:
The Puls Atlas CLI tool for managing Atlas projects
184 lines • 9.87 kB
JavaScript
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
};