From 39aad9780106a3288ce1c004dd64bade4dfc2653 Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Tue, 27 Aug 2024 15:11:18 +0200 Subject: [PATCH 1/2] feat: remove the collie kit compile command We figured out this use case is better served by tools like pre-commit-terraform https://github.com/antonbabenko/pre-commit-terraform?tab=readme-ov-file#terraform_validate which includes more robust handling of terraform's quirks. Given that there are better solutions out there, we think it's best to remove this feature from collie and focus our resources on unique features. Additionally, using "terraform validate" is a bad fit for validating kit modules as it has big problems with configuration_aliases https://github.com/hashicorp/terraform/issues/28490 It seems that it is much better suited to validating platform modules instead. --- src/api/CliApiFacadeFactory.ts | 15 ----- src/api/terraform/TerraformCliFacade.ts | 22 ------- src/commands/kit/compile.command.ts | 87 ------------------------- src/commands/kit/kit.command.ts | 2 - 4 files changed, 126 deletions(-) delete mode 100644 src/api/terraform/TerraformCliFacade.ts delete mode 100644 src/commands/kit/compile.command.ts diff --git a/src/api/CliApiFacadeFactory.ts b/src/api/CliApiFacadeFactory.ts index b9217e40..d3ffa3bb 100644 --- a/src/api/CliApiFacadeFactory.ts +++ b/src/api/CliApiFacadeFactory.ts @@ -114,9 +114,6 @@ export class CliApiFacadeFactory { return azure; } - // buildCustom() { - // } - public buildGit() { const detectorRunner = this.buildQuietLoggingProcessRunner(); const detector = new GitCliDetector(detectorRunner); @@ -166,18 +163,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/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/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) From 73eef4ba2001a5b7d4fc8976abab81c5a8be444a Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Tue, 27 Aug 2024 15:05:34 +0200 Subject: [PATCH 2/2] feat: make collie info detect opentofu or terraform closes #289 --- src/api/CliApiFacadeFactory.ts | 8 +++-- src/api/CliDetector.ts | 7 +++- src/api/terraform/OpenTofuCliDetector.ts | 17 ++++++++++ .../terraform/TofuOrTerraformCliDetector.ts | 32 +++++++++++++++++++ 4 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 src/api/terraform/OpenTofuCliDetector.ts create mode 100644 src/api/terraform/TofuOrTerraformCliDetector.ts diff --git a/src/api/CliApiFacadeFactory.ts b/src/api/CliApiFacadeFactory.ts index d3ffa3bb..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), 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/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); + } + } +}