diff --git a/src/api/CliApiFacadeFactory.ts b/src/api/CliApiFacadeFactory.ts index b9217e40..8b98b6d7 100644 --- a/src/api/CliApiFacadeFactory.ts +++ b/src/api/CliApiFacadeFactory.ts @@ -35,7 +35,8 @@ import { TerraformDocsCliFacade } from "./terraform-docs/TerraformDocsCliFacade. import { CollieRepository } from "../model/CollieRepository.ts"; import { GitCliDetector } from "./git/GitCliDetector.ts"; import { GitCliFacade } from "./git/GitCliFacade.ts"; -import { TerraformCliFacade } from "./terraform/TerraformCliFacade.ts"; +import { TofuOrTerraformCliDetector } from "./terraform/TofuOrTerraformCliDetector.ts"; +import { OpenTofuCliDetector } from "./terraform/OpenTofuCliDetector.ts"; export class CliApiFacadeFactory { constructor( @@ -49,7 +50,10 @@ export class CliApiFacadeFactory { new AzCliDetector(processRunner), new GcloudCliDetector(processRunner), new GitCliDetector(processRunner), - new TerraformCliDetector(processRunner), + new TofuOrTerraformCliDetector( + new OpenTofuCliDetector(processRunner), + new TerraformCliDetector(processRunner), + ), new TerragruntCliDetector(processRunner), new TerraformDocsCliDetector(processRunner), new NpmCliDetector(processRunner), @@ -114,9 +118,6 @@ export class CliApiFacadeFactory { return azure; } - // buildCustom() { - // } - public buildGit() { const detectorRunner = this.buildQuietLoggingProcessRunner(); const detector = new GitCliDetector(detectorRunner); @@ -166,18 +167,6 @@ export class CliApiFacadeFactory { return new TerraformDocsCliFacade(repo, processRunner); } - public buildTerraform() { - const quietRunner = this.buildQuietLoggingProcessRunner(); - const detector = new TerraformCliDetector(quietRunner); - - const processRunner = this.wrapFacadeProcessRunner( - quietRunner, - new ProcessRunnerErrorResultHandler(detector), - ); - - return new TerraformCliFacade(processRunner); - } - // DESIGN: we need to build up the ProcessRunner behavior in the following order (from outer to inner) // - DefaultsProcessRunnerDecorator -> customise the command that gets run // - ResultHandlerProcessRunnerDecorator -> retry/print error on what actually ran diff --git a/src/api/CliDetector.ts b/src/api/CliDetector.ts index a61cdcc7..41e37238 100644 --- a/src/api/CliDetector.ts +++ b/src/api/CliDetector.ts @@ -19,7 +19,12 @@ export type CliDetectionResult = info: string; }; -export abstract class CliDetector { +export interface ICliDetector { + tryRaiseInstallationStatusError(): Promise; + detect(): Promise; +} + +export abstract class CliDetector implements ICliDetector { constructor( protected readonly cli: string, protected readonly runner: IProcessRunner, diff --git a/src/api/terraform/OpenTofuCliDetector.ts b/src/api/terraform/OpenTofuCliDetector.ts new file mode 100644 index 00000000..c7c9326f --- /dev/null +++ b/src/api/terraform/OpenTofuCliDetector.ts @@ -0,0 +1,17 @@ +import { IProcessRunner } from "../../process/IProcessRunner.ts"; +import { ProcessResultWithOutput } from "../../process/ProcessRunnerResult.ts"; +import { CliDetector } from "../CliDetector.ts"; + +export class OpenTofuCliDetector extends CliDetector { + constructor(runner: IProcessRunner) { + super("tofu", runner); + } + + protected parseVersion(versionCmdOutput: string): string { + return versionCmdOutput.split("\n")[0].substring("OpenTofu ".length); + } + + protected isSupportedVersion(version: string): boolean { + return CliDetector.testSemverSatisfiesRange(version, ">=1.0.0"); + } +} diff --git a/src/api/terraform/TerraformCliFacade.ts b/src/api/terraform/TerraformCliFacade.ts deleted file mode 100644 index 6aab5e78..00000000 --- a/src/api/terraform/TerraformCliFacade.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ProcessResult } from "../../process/ProcessRunnerResult.ts"; -import { IProcessRunner } from "../../process/IProcessRunner.ts"; - -export class TerraformCliFacade { - constructor(private runner: IProcessRunner) {} - - async init(path: string, opts: { backend: boolean }) { - const cmds = ["terraform", "init"]; - - if (!opts.backend) { - cmds.push("-backend=false"); - } - - return await this.runner.run(cmds, { cwd: path }); - } - - async validate(path: string) { - const cmds = ["terraform", "validate"]; - - return await this.runner.run(cmds, { cwd: path }); - } -} diff --git a/src/api/terraform/TofuOrTerraformCliDetector.ts b/src/api/terraform/TofuOrTerraformCliDetector.ts new file mode 100644 index 00000000..4d0c168c --- /dev/null +++ b/src/api/terraform/TofuOrTerraformCliDetector.ts @@ -0,0 +1,32 @@ +import { CliDetectionResult, ICliDetector } from "../CliDetector.ts"; +import { InstallationStatus } from "/api/CliInstallationStatus.ts"; +import { CliInstallationStatusError } from "/errors.ts"; +import { OpenTofuCliDetector } from "./OpenTofuCliDetector.ts"; +import { TerraformCliDetector } from "./TerraformCliDetector.ts"; + +export class TofuOrTerraformCliDetector implements ICliDetector { + constructor( + private readonly tofu: OpenTofuCliDetector, + private readonly terraform: TerraformCliDetector, + ) { + } + async detect(): Promise { + const tofuResult = await this.tofu.detect(); + if (tofuResult.status === InstallationStatus.Installed) { + return tofuResult; + } + + return this.terraform.detect(); + } + + async tryRaiseInstallationStatusError() { + const { status } = await this.detect(); + switch (status) { + case InstallationStatus.Installed: + break; + case InstallationStatus.NotInstalled: + case InstallationStatus.UnsupportedVersion: + throw new CliInstallationStatusError("tofu or terraform", status); + } + } +} diff --git a/src/commands/kit/compile.command.ts b/src/commands/kit/compile.command.ts deleted file mode 100644 index 7891600f..00000000 --- a/src/commands/kit/compile.command.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { pooledMap } from "std/async"; - -import { CollieRepository } from "../../model/CollieRepository.ts"; -import { GlobalCommandOptions } from "../GlobalCommandOptions.ts"; -import { Logger } from "../../cli/Logger.ts"; -import { ModelValidator } from "../../model/schemas/ModelValidator.ts"; -import { KitModuleRepository } from "../../kit/KitModuleRepository.ts"; -import { TopLevelCommand } from "../TopLevelCommand.ts"; -import { CliApiFacadeFactory } from "../../api/CliApiFacadeFactory.ts"; -import { ProgressReporter } from "../../cli/ProgressReporter.ts"; -import { MeshError } from "../../errors.ts"; - -// limit concurrency -const concurrencyLimit = navigator.hardwareConcurrency; - -export function registerCompileCmd(program: TopLevelCommand) { - program - .command("compile [module]") - .description( - "Compile kit modules, validating their terraform and updating documentation", - ) - .action(async (opts: GlobalCommandOptions, module?: string) => { - const collie = await CollieRepository.load(); - const logger = new Logger(collie, opts); - const validator = new ModelValidator(logger); - const moduleRepo = await KitModuleRepository.load( - collie, - validator, - logger, - ); - - const kitProgress = new ProgressReporter("compiling", "kit", logger); - - // todo: should compiling a kit module also run tflint and other stuff? - const factory = new CliApiFacadeFactory(logger); - const tf = factory.buildTerraform(); - const tfDocs = factory.buildTerraformDocs(collie); - - const modules = moduleRepo.all.filter((x) => !module || module == x.id); - - const iterator = pooledMap(concurrencyLimit, modules, async (x) => { - const moduleProgress = new ProgressReporter( - "compiling", - x.kitModulePath, - logger, - ); - - try { - await tfDocs.updateReadme(x.kitModulePath); - - const resolvedKitModulePath = collie.resolvePath(x.kitModulePath); - - // for checking we need to no locks (they only confuse tfdocs) and also no backend providers - await tf.init(resolvedKitModulePath, { backend: false }); - await tf.validate(resolvedKitModulePath); - } catch (error) { - moduleProgress.failed(); - - // log then throw is typically an anti-pattern, but its fine here - // since an error here will end up as an AggregateError later below - logger.error( - (_) => - `encountered error compiling kit module ${x.kitModulePath}\n${error}`, - ); - - throw error; - } - - moduleProgress.done(); - }); - - try { - for await (const _ of iterator) { - // consume iterator - } - } catch (ex) { - if (ex instanceof AggregateError) { - // exiting here is fine since the map function logs all erors - throw new MeshError("validating kit modules failed"); - } - - throw ex; - } - - kitProgress.done(); - }); -} diff --git a/src/commands/kit/kit.command.ts b/src/commands/kit/kit.command.ts index 67c1559d..f3e72077 100644 --- a/src/commands/kit/kit.command.ts +++ b/src/commands/kit/kit.command.ts @@ -1,6 +1,5 @@ import { makeTopLevelCommand, TopLevelCommand } from "../TopLevelCommand.ts"; import { registerApplyCmd } from "./apply.command.ts"; -import { registerCompileCmd } from "./compile.command.ts"; import { registerImportCmd } from "./import.command.ts"; import { registerNewCmd } from "./new.command.ts"; import { registerTreeCmd } from "./tree.command.ts"; @@ -11,7 +10,6 @@ export function registerKitCommand(program: TopLevelCommand) { registerApplyCmd(kitCommands); registerImportCmd(kitCommands); registerTreeCmd(kitCommands); - registerCompileCmd(kitCommands); program .command("kit", kitCommands)