Skip to content

Materialize target release configuration before install using a temporary peer #13

Description

@ausimian

Summary

Make Castle stop owning config-provider execution. Materialize the target release's runtime configuration as part of install by starting a temporary BEAM that uses the target release's code and normal Elixir Config.Provider machinery.

This issue is the authoritative end-state for runtime configuration handling. Any existing Castle.generate/1 / Forecastle build.config -> sys.config boot hook should be treated as transitional compatibility code to be removed or reduced as this lands.

Motivation

The current Castle.generate/1 reads build.config, invokes stored config providers in the currently running node, and writes releases/<vsn>/sys.config.

That has two architectural problems:

  1. Installing release B while A is running can execute A's provider modules while trying to configure B.
  2. Castle/Forecastle duplicates Elixir's own config-provider lifecycle, creating an increasingly large parallel implementation to maintain.

The target architecture is:

normal cold start
  -> stock Mix launcher
  -> normal Elixir Config.Provider handling

Castle.install(B)
  -> start temporary peer using B's code/boot paths
  -> run B's normal Config.Provider pipeline
  -> persist final B sys.config for release_handler
  -> stop peer
  -> release_handler.install_release(B)

Proposed direction

Treat target config materialization as an internal step of Castle.install/1:

  1. Ensure the target release has been unpacked.
  2. Start a temporary BEAM using :peer and the target release's boot/code paths.
  3. Prefer connection: :standard_io so no EPMD, cookie, or temporary distributed node name is required.
  4. Let the target release execute its ordinary Elixir config-provider pipeline.
  5. Persist the final resolved configuration to releases/<vsn>/sys.config.
  6. Stop the peer.
  7. Continue with :release_handler.install_release/1.

A Castle-owned final provider such as Castle.ConfigWriter may be useful as a sink for the fully resolved config, but Castle should not manually reproduce the provider reduction loop.

Explicit non-goals

  • Do not extend the old Castle.generate/1 design into a more general long-term config engine.
  • Do not require Forecastle to keep special-casing runtime_config_path or arbitrary provider arguments once normal Mix/Elixir handling can own those semantics again.
  • Do not introduce another operator command; config materialization belongs inside install.

Acceptance criteria

  • Installing release B while A is running executes B's config-provider code.
  • releases/B/sys.config exists before B is installed.
  • The temporary VM is cleaned up on success and failure.
  • The mechanism does not require distributed Erlang.
  • Normal cold starts use the stock Mix launcher and normal Elixir runtime configuration semantics.
  • Castle.generate/1 and Forecastle's bespoke pre-start config-generation path are removed or reduced to transitional/internal code no longer used for the normal boot path.

Scope update (1.0.0 planning)

This is the keystone of the 1.0.0 work, not an incremental improvement.

forecastle#3 removed the forked launcher, but deliberately left the boot-time
configuration machinery in place as a transitional measure: the env.sh fragment
still runs a preboot VM on every start/daemon/eval to expand build.config
into sys.config via Castle.generate/1. That is Castle and Forecastle
reimplementing Elixir's config-provider pipeline. This issue is what licenses its
removal.

Intended end state:

normal cold start                    Castle install B
    │                                    │
    ▼                                    ▼
stock Mix launcher               temporary peer using B's code
    │                                    │
    ▼                                    ▼
normal Config.Provider           normal Config.Provider handling
handling                                 │
                                         ▼
                                 materialise B/sys.config
                                         │
                                         ▼
                                 release_handler.install_release(B)

Elixir's Config.Provider stays authoritative in both paths. Castle must not
reimplement run_providers/2 — the point of this issue is to stop having a second
implementation, so a solution that hand-rolls the reduction loop in the peer has
missed it. Castle.generate/1 goes away as a public function.

Prefer keeping the mechanism inside Castle rather than adding a
Castle.ConfigWriter provider that Forecastle has to install: the target release
already ships its own preboot boot script (which includes :sasl, :compiler,
:elixir, :castle), so the peer can boot that and call into Castle directly.

Hard dependency: forecastle#6. The peer can only run "the target release's
normal config-provider pipeline" if the target has one, which requires Forecastle
to stop stripping providers and renaming sys.config to build.config. Conversely
forecastle#6 alone breaks install/1, which still reads build.config. The two
changes are atomic and must land together.

Worth a test rather than an assumption: once Forecastle stops stripping providers,
the target's sys.config carries Mix's RUNTIME_CONFIG=true marker, so a later
cold boot of that version re-runs providers over the materialised file. That is
probably correct behaviour, but the hot-install path and the cold-boot path now
write the same file, and the interaction should be verified rather than assumed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions