Skip to main content

Octo

The entry point to Octo.

You describe infrastructure by loading modules, and Octo turns that description into terraform, checks what terraform intends to do, and records what it did. That is the lifecycle, and it is always these four steps:

  1. Octo.generate - write the terraform module folders for the infrastructure you described.
  2. Octo.validate - correlate terraform's plans with your intent, and report the difference.
  3. terraform apply - see TerraformUtility. Octo does not do this for you.
  4. Octo.commit - record the applied result back into Octo's state.

Splitting them is deliberate: nothing is applied until you have seen a report of what would change, and each step is a separate process, so a CI pipeline can gate between them.

@example
const octo = new Octo();
await octo.initialize(new LocalStateProvider('./infrastructure'));

octo.registerTerraformConfig({ providers: { aws: { source: 'hashicorp/aws' } } });
octo.registerTerraformProvider('aws', '1234567890', 'us-east-1');

octo.loadModule(SimpleAppModule, 'app-module', { name: 'my-app' });
await octo.compose();

await octo.generate(octo.getRootModel(), { outputDir: './infrastructure/terraform' });
@group

Main

@see

Definition of Modules.

Index

Constructors

constructor

Methods

commit

  • commit(app: App, options: { lockId: string; outputs: Map<string, TerraformOutputs>; partial?: boolean }, transaction?: ModelTransactionResult): Promise<Pick<{ emptiedModuleIds: string[]; externalResourceIds: string[]; modelTransaction: DiffMetadata[][]; resources: { action: ADD | DELETE | REPLACE | UPDATE; moduleId?: string; previousTerraformAddresses: string[]; reason?: string; resourceContext: string; resourceId: string; terraformAddresses: string[] }[]; responses: { key: string; previousValue?: string; resourceContext: string; value: string }[]; warnings: { message: string; moduleId?: string }[] }, emptiedModuleIds | warnings>>
  • The last step of the lifecycle - records the result of a terraform apply back into Octo's state.

    A terraform apply reports what it created, and part of that is what each octo resource's response is made of - ARNs, generated IDs, endpoints. Without this step the next run has no idea any of it exists.

    All-or-nothing: if an expected output is missing, this throws before touching state, so a partial apply never leaves a half-written record behind. Pass partial after an apply that failed and the missing outputs become warnings instead, with every resource they belong to left out of the committed graph. Outputs for a folder Octo does not track are ignored, and returned as warnings for you to surface. The modules whose folders were emptied are returned as emptiedModuleIds, for TerraformUtility.deleteDestroyedFolders; one whose terraform state still holds resources stays recorded, so the next generate writes it again.

    Requires the lock, and does not release it. Taking and releasing the lock belongs to whoever drives the lifecycle - the CLI's octo commit releases it once this returns, and keeps it when this throws.

    @throws

    TransactionError When the app is not locked, or lockId is not the lock held.


    Parameters

    • app: App

      The root model, from Octo.getRootModel.

    • options: { lockId: string; outputs: Map<string, TerraformOutputs>; partial?: boolean }

      lockId is the lock from octo lock. outputs is TerraformUtility.output(dir, { json: true }) in full. partial records what a failed apply did build.

      • lockId: string
      • outputs: Map<string, TerraformOutputs>
      • optionalpartial: boolean
    • optionaltransaction: ModelTransactionResult

      The model transaction result.

    Returns Promise<Pick<{ emptiedModuleIds: string[]; externalResourceIds: string[]; modelTransaction: DiffMetadata[][]; resources: { action: ADD | DELETE | REPLACE | UPDATE; moduleId?: string; previousTerraformAddresses: string[]; reason?: string; resourceContext: string; resourceId: string; terraformAddresses: string[] }[]; responses: { key: string; previousValue?: string; resourceContext: string; value: string }[]; warnings: { message: string; moduleId?: string }[] }, emptiedModuleIds | warnings>>

compose

  • compose(): Promise<{}>
  • Runs every loaded module, in the order Octo.orderModules set, building the model graph. Call it once, after loading modules and before any lifecycle step.


    Returns Promise<{}>

    Every model and overlay the modules created, keyed by a unique ID.

extendState

  • To build this app on top of another app's state, without ever changing it.

    Every model, overlay and resource the other app recorded is declared here under the module that recorded it, as a test declares what already exists, so this app's modules reference them as if they were declared alongside them. Nothing of the other app runs: its nodes take no action, its resources render no terraform, and a reference to one reads the response that app committed. Every class its state names must be loaded. Call it after Octo.initialize and before loading this app's own modules.

    @throws

    ModuleError When the newest sealed state holds no models, or nothing is sealed yet.

    @throws

    NodeUnsynthError When a class the state names is not loaded.


    Parameters

    • stateProvider: IStateProvider

      Where the other app's state is read from: its newest sealed state, never a run still under way. This app writes none of its own state there.

    Returns Promise<void>

generate

  • generate(app: App, options: { outputDir: string }, transaction?: ModelTransactionResult): Promise<DiffMetadata[][]>
  • The first step of the lifecycle - writes the terragrunt module folders for the infrastructure you described, one folder per module, and saves the model state.

    A new module gets a new folder, and a changed module is rewritten in the folder it already has. A module you removed is not deleted: its folder is emptied, keeping only the provider blocks, so the next terraform apply destroys the live infrastructure rather than orphaning it. Generate never deletes a folder.


    Parameters

    • app: App

      The root model, from Octo.getRootModel.

    • options: { outputDir: string }

      outputDir is where the module folders are written.

      • outputDir: string
    • optionaltransaction: ModelTransactionResult

      The model transaction result.

    Returns Promise<DiffMetadata[][]>

getModule

  • getModule<M>(...args: [moduleId: string]): M | undefined
  • To fetch a loaded module by its module ID.


    Type parameters

    • M: UnknownModule

    Parameters

    • rest...args: [moduleId: string]

    Returns M | undefined

    The module, or undefined if no module was loaded under that ID.

getModuleResources

  • getModuleResources(...args: [moduleId: string]): UnknownResource[]
  • To fetch the resources a module produced, by its module ID.


    Parameters

    • rest...args: [moduleId: string]

    Returns UnknownResource[]

getRootModel

  • getRootModel(): App
  • The root of the model graph - the one App the loaded modules produced. Every lifecycle step takes it. Call after Octo.compose.

    @throws

    If no app model, or more than one distinct app model, was registered.


    Returns App

initialize

  • initialize(stateProvider: IStateProvider, initializeInContainer?: { options?: { args?: unknown[]; metadata?: {} }; type: string | Constructable<unknown> }[], excludeInContainer?: { type: string | Constructable<unknown> }[]): Promise<void>
  • Boots Octo. Call it once, before anything else.

    The two optional arguments are how you replace Octo's internals with your own: exclusions run first, so you can unregister a built-in Factory and then resolve yours in its place.


    Parameters

    • stateProvider: IStateProvider

      Where Octo reads and writes its state. See IStateProvider.

    • initializeInContainer: { options?: { args?: unknown[]; metadata?: {} }; type: string | Constructable<unknown> }[] = []

      Factories to resolve at boot - event listeners, most commonly.

    • excludeInContainer: { type: string | Constructable<unknown> }[] = []

      Factories to unregister at boot.

    Returns Promise<void>

loadModule

  • To load a module, which Octo.compose will then run.

    The same module class can be loaded more than once under different module IDs, and the ID scopes everything that instance produces.

    @throws

    ModuleError When the module ID holds :, or an imported state already uses it.


    Type parameters

    • M: UnknownModule

    Parameters

    • module: string | Constructable<M>

      The module class, or its registered name.

    • moduleId: string

      A unique ID for this instance of the module.

    • inputs: ModuleSchemaInputs<M>

      The inputs, matching the module's schema.

    Returns void

orderModules

  • orderModules(...args: [modules: (string | Constructable<UnknownModule>)[]]): void
  • To set the order Octo.compose runs modules in, by listing their classes.

    Order matters because a module resolves what earlier modules produced - a subnet module must run after the region module whose region it attaches to.


    Parameters

    • rest...args: [modules: (string | Constructable<UnknownModule>)[]]

    Returns void

readImportsAsGenerated

  • readImportsAsGenerated(): Promise<void>
  • To read every state this app extends and every store as this run's generate read them, so the commands of one run see one version of each other app. Call it before Octo.extendState and Octo.readStores in every command of a run but generate. An import that generate did not read is read from its newest sealed state.


    Returns Promise<void>

readStores

  • readStores(stores: { name: string; stateProvider: IStateProvider }[]): Promise<void>
  • To read other apps' states as stores, each a graph of its own that this app builds next to, never on.

    A store's models, overlays and resources are declared under its name, so a module input takes one as ${{<name>:<moduleId>.model.<node>}} and walks from it as from any other. A store is read-only: nothing of it runs, renders terraform or is saved here, and adding a child or a link to one of its nodes fails. This app keeps an app of its own, and saves its own links to a store's resources but never its links to a store's models. A link a store holds into a state not read here is dropped. Every class a store names must be loaded. Call it once, with every store, after Octo.initialize and before loading this app's own modules.

    @throws

    ModuleError When a name repeats or holds more than letters, digits, _ and -, when a store has sealed no models, or when two stores hold the same node.

    @throws

    NodeUnsynthError When a class a store names is not loaded.


    Parameters

    • stores: { name: string; stateProvider: IStateProvider }[]

      Each store's name, and where its newest sealed state is read from. This app writes none of its own state there.

    Returns Promise<void>

registerHooks

  • registerHooks(...args: [{ postCommitHooks?: PostHookSignature[]; postModelActionHooks?: PostHookSignature[]; postResourceActionHooks?: PostHookSignature[]; preCommitHooks?: PreHookSignature[]; preModelActionHooks?: PreHookSignature[]; preResourceActionHooks?: PreHookSignature[] }?]): void
  • To register callbacks that run at fixed points of the lifecycle. Use them to inject behavior - an audit trail, an approval gate - without modifying the framework.

    The hook points, each with its own signature:

    • PreCommitHook / PostCommitHook - around the commit.
    • PreModelActionHook / PostModelActionHook - around every model action.
    • PreResourceActionHook / PostResourceActionHook - around every resource action.
    @see

    Techniques for Hooks.


    Parameters

    • rest...args: [{ postCommitHooks?: PostHookSignature[]; postModelActionHooks?: PostHookSignature[]; postResourceActionHooks?: PostHookSignature[]; preCommitHooks?: PreHookSignature[]; preModelActionHooks?: PreHookSignature[]; preResourceActionHooks?: PreHookSignature[] }?]

    Returns void

registerPackageVersions

  • registerPackageVersions(...args: [packageVersions: {}]): void
  • To record the version of each package this app's classes come from, octo's included, in its state. A state this app extends, or a store it reads, generated on another version of a package both use, is refused: its classes may read the other's state wrongly.


    Parameters

    • rest...args: [packageVersions: {}]

    Returns void

registerSchemaTranslation

  • registerSchemaTranslation(...args: [from: Constructable<unknown>, to: Constructable<unknown>, translator: (synth: unknown) => unknown]): void
  • To teach Octo how to read one schema as another.

    Modules find each other by schema, so a module that changes its schema breaks its consumers. A translation is the escape hatch: register the schema you have, the schema your module expects, and a function between them, and the lookup keeps working while you migrate.

    @see

    Techniques for Schema.


    Parameters

    Returns void

registerTags

  • registerTags(args: { scope: { moduleId?: string; resourceContext?: string }; tags: {} }[]): void
  • To apply tags to resources in bulk, so that individual modules do not each have to be told about your organization's tagging policy.

    Each entry's scope decides its reach: empty tags every resource of the app, moduleId tags every resource of one module, and resourceContext tags a single resource.


    Parameters

    • args: { scope: { moduleId?: string; resourceContext?: string }; tags: {} }[]

    Returns void

registerTerraformConfig

  • registerTerraformConfig(...args: [{ minTerraformVersion?: string; providers?: Record<string, { minVersion?: string; source: string }> }?]): void
  • To set the minimum terraform version, and the required_providers block rendered into every generated module folder.

    Call it after Octo.initialize, and before Octo.generate.


    Parameters

    • rest...args: [{ minTerraformVersion?: string; providers?: Record<string, { minVersion?: string; source: string }> }?]

    Returns void

registerTerraformProvider

  • registerTerraformProvider(...args: [providerType: string, accountId: string, regionId: string, spec: Record<string, unknown>, { accountName?: string; setRegionAttribute?: boolean }?]): void
  • To register a terraform provider for one deployment target - a provider type, an account, and a region.

    Resources bind to a provider by { accountId, regionId } alone, so this is the one place a provider type is named. Register one per account/region combination you deploy into.

    Call it after Octo.initialize, and before Octo.generate.


    Parameters

    • rest...args: [providerType: string, accountId: string, regionId: string, spec: Record<string, unknown>, { accountName?: string; setRegionAttribute?: boolean }?]

    Returns void

validate

  • validate(app: App, plans: { errors?: Map<string, string>; plans: Map<string, TerraformPlan> }, transaction?: ModelTransactionResult): Promise<ValidateReport>
  • The second step of the lifecycle - correlates terraform's plans with your intent, and reports what it finds. This is what you read before deciding whether to apply.

    The report answers two questions separately:

    1. What will change, and why. Including terraform changes Octo did not flag directly but can attribute to something it did - a referenced resource whose identity moved, or an address the resource no longer renders.
    2. What changed that your intent does not explain - drift, i.e. infrastructure modified outside Octo. Read confidence first: drift is only reliable when intent itself is unchanged.

    The report fails only when terraform could not plan a folder Octo generated. Folders Octo does not track are excluded, and noted. So is every external resource a previous run published and no commit has recorded, which is the only step that reads those records back for a user.


    Parameters

    • app: App

      The root model, from Octo.getRootModel.

    • plans: { errors?: Map<string, string>; plans: Map<string, TerraformPlan> }

      The whole result of TerraformUtility.plan(dir, { json: true }). Its errors map carries terraform's own message for each folder that failed to plan.

      • optionalerrors: Map<string, string>
      • plans: Map<string, TerraformPlan>
    • optionaltransaction: ModelTransactionResult

      The model transaction result.

    Returns Promise<ValidateReport>