Skip to content

Commit 37ae759

Browse files
authored
Merge pull request #18 from ausimian/issue/13-peer-config
feat: expand target configuration in a peer of its own (castle#13, step 1 of 3)
2 parents 7a0a659 + 814cb35 commit 37ae759

12 files changed

Lines changed: 2418 additions & 24 deletions

File tree

AGENTS.md

Lines changed: 260 additions & 16 deletions
Large diffs are not rendered by default.

RELEASE.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,53 @@
11
### Added
22

3+
- The configuration of the version being installed is now expanded by running
4+
*that version's* config providers, in a temporary VM booted from that
5+
version's own boot script on its own emulator, rather than by running provider
6+
state stashed at build time in the version that happens to be running. A
7+
provider module can differ between the two — which is precisely what an
8+
upgrade may change — and only the target's own answer is the right one. It
9+
also leaves Elixir's `Config.Provider` as the single implementation of the
10+
provider pipeline: Castle drives it and no longer keeps a copy of it.
11+
12+
The temporary VM needs no epmd, no cookie, no node name and no distributed
13+
Erlang: it talks to the running node over a socket on the loopback interface,
14+
and whatever it prints — a provider explaining what it could not find, say —
15+
arrives on the terminal that asked for the install. It is stopped on every way
16+
out, including every failing one, and it cannot hold an install open: both its
17+
boot and the work it is asked to do have deadlines. Everything that can refuse
18+
to go on refuses before the upgrade is applied, so configuration that cannot
19+
be expanded leaves an install that did not happen rather than one that
20+
half did.
21+
22+
Each expansion starts from the configuration the release was built with, which
23+
the first one copies aside as `sys.config.pristine` and none of them
24+
overwrites. Config providers are not obliged to be idempotent, and the
25+
familiar ones are not: a `runtime.exs` that sets a key only when an
26+
environment variable is present says nothing about that key when it is absent,
27+
so expanding over the previous result would leave a value behind after the
28+
provider had stopped supplying it — and the version made permanent would be
29+
configured differently from the way it goes on to boot. Expanding from the
30+
original instead means installing and then committing produce the same answer
31+
a boot would, which is the point of expanding at either. That copy is made
32+
atomically and with the permissions `sys.config` has, so a partly written one
33+
can never be found and read, and restricting `sys.config` — as an operator
34+
might, since it holds credentials — restricts this too.
35+
36+
Among the things that refuse is the check Elixir makes on a configuration
37+
before booting into it: that what `Application.compile_env/3` read when the
38+
release was compiled is what the resolved configuration says now. A version
39+
whose runtime configuration contradicts what it was compiled against is
40+
refused here, where refusing costs nothing, rather than accepted and then
41+
found to be unbootable — which, for an upgrade that restarts, is found on the
42+
way back up with a rollback as the only way out.
43+
44+
Which way a release is configured is settled by the release itself. One whose
45+
configuration was intercepted at build time — every release assembled by the
46+
Forecastle this is released alongside, recognisable by the `build.config` in
47+
its version directory — is expanded exactly as it was before, so nothing about
48+
installing or committing such a release changes. The new path is taken by a
49+
release whose ordinary Mix provider pipeline is intact, which is the shape
50+
Forecastle stops interfering with in its own next release.
351
- `Castle.Error`, the exception raised by a release-management command that did
452
not succeed.
553
- `Castle.running/1`, which succeeds when the version it is given is the

lib/castle.ex

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ defmodule Castle do
3434
end
3535

3636
def install(vsn) when is_binary(vsn) do
37-
generate(vsn)
37+
materialise(vsn)
3838
report!(Commands.install(vsn))
3939
end
4040

@@ -43,7 +43,7 @@ defmodule Castle do
4343
end
4444

4545
def commit(vsn) when is_binary(vsn) do
46-
generate(vsn)
46+
materialise(vsn)
4747
report!(Commands.commit(vsn))
4848
end
4949

@@ -55,10 +55,20 @@ defmodule Castle do
5555
report!(Commands.releases())
5656
end
5757

58-
# The version directory of the running release. Where the configuration is
59-
# written is derived from the release that is running, never chosen by the
60-
# caller - see castle#13, which materialises target configuration in a peer
61-
# rather than extending this path.
58+
# Makes sure the target version's configuration exists before the version is
59+
# handed to `:release_handler`, and fails here if it cannot be made to. It
60+
# runs ahead of both operations that need it, and everything that can refuse
61+
# to go on - a peer that will not start, a boot script that is not there, a
62+
# provider that raises - refuses from inside this call, which is to say before
63+
# `install_release/1` has been asked for anything. Nothing after that point
64+
# may fail without saying that an install happened.
65+
defp materialise(vsn), do: report!(Commands.materialise(rel_vsn_dir(vsn)))
66+
67+
# The version directory of the release being operated on, under the root of
68+
# the release that is running. Derived, never chosen by the caller: which file
69+
# the configuration lands in is a property of the installation, not an
70+
# argument. It resolves for any version the running release knows about,
71+
# because `:release_handler` unpacks every version into this same root.
6272
defp rel_vsn_dir(vsn), do: Path.join([:code.root_dir(), "releases", vsn])
6373

6474
defp report!({:ok, lines}), do: Enum.each(lines, &IO.puts/1)

lib/castle/commands.ex

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,46 @@ defmodule Castle.Commands do
6666
end
6767
end
6868

69+
@doc """
70+
Materialises the configuration of the release in `rel_vsn_dir`.
71+
72+
Two shapes of release reach this, and the presence of `build.config` is what
73+
tells them apart. Forecastle used to intercept configuration at assembly time:
74+
it stripped the providers out of the release, stashed their initialised state
75+
under this application's key, and renamed the `sys.config` Mix had written to
76+
`build.config`. The only thing that can expand a release assembled that way is
77+
`generate/1`, folding the stashed state over the file it was taken from - and
78+
the presence of `build.config` is the test rather than the absence of
79+
`sys.config`, because from its first boot onwards such a release has both:
80+
writing one beside the other is what `generate/1` does.
81+
82+
A release Mix configured normally has its providers where Mix put them and its
83+
`sys.config` under the name Mix gave it, and nothing has been renamed - so the
84+
absence of `build.config` says the pipeline is intact, and the target can be
85+
evaluated the way Elixir intends: in a VM of its own, running its own
86+
providers, which is what `Castle.Peer` does. That is the only sound way to do
87+
it, since a provider module can differ between the version that is running and
88+
the version being installed.
89+
90+
The module is an argument for the same reason `:release_handler` is: so that a
91+
test can watch which way the decision went without starting a VM.
92+
"""
93+
@spec materialise(Path.t(), module()) :: result()
94+
def materialise(rel_vsn_dir, peer \\ Castle.Peer) do
95+
cond do
96+
File.exists?(Path.join(rel_vsn_dir, "build.config")) ->
97+
generate(rel_vsn_dir)
98+
99+
File.dir?(rel_vsn_dir) ->
100+
peer.materialise(rel_vsn_dir)
101+
102+
true ->
103+
{:error,
104+
"Cannot configure #{Path.basename(rel_vsn_dir)}: #{rel_vsn_dir} does not exist. " <>
105+
"Unpack the release first."}
106+
end
107+
end
108+
69109
@doc """
70110
Expands the build-time configuration in `rel_vsn_dir` into its `sys.config`.
71111

0 commit comments

Comments
 (0)