| 2 |
2
|
|
use ExUnit.Case, async: false
|
| 3 |
3
|
|
|
| 4 |
4
|
|
alias OpenAgents.Forge.RelupNode
|
|
5
|
+ |
alias OpenAgents.ReleaseState
|
|
6
|
+ |
alias OpenAgents.ReleaseState.State
|
| 5 |
7
|
|
alias OpenAgents.Test.ReleaseHandler
|
| 6 |
8
|
|
|
| 7 |
|
- |
setup do
|
| 8 |
|
- |
start_supervised!(
|
| 9 |
|
- |
{ReleaseHandler, %{releases: [{~c"openagents", ~c"0.1.0", [], :permanent}]}}
|
| 10 |
|
- |
)
|
|
9
|
+ |
describe "a schema 1 to schema 2 upgrade" do
|
|
10
|
+ |
setup do
|
|
11
|
+ |
start_supervised!(
|
|
12
|
+ |
{ReleaseHandler, %{releases: [{~c"openagents", ~c"0.1.0", [], :permanent}]}}
|
|
13
|
+ |
)
|
| 11 |
14
|
|
|
| 12 |
|
- |
root =
|
| 13 |
|
- |
Path.join(System.tmp_dir!(), "openagents-relup-node-#{System.unique_integer([:positive])}")
|
|
15
|
+ |
root = release_root()
|
| 14 |
16
|
|
|
| 15 |
|
- |
File.mkdir_p!(Path.join(root, "releases"))
|
| 16 |
|
- |
on_exit(fn -> File.rm_rf!(root) end)
|
|
17
|
+ |
artifact = "immutable release artifact"
|
|
18
|
+ |
|
|
19
|
+ |
request = %{
|
|
20
|
+ |
release_name: "openagents",
|
|
21
|
+ |
from_version: "0.1.0",
|
|
22
|
+ |
to_version: "0.2.0",
|
|
23
|
+ |
from_state_version: 1,
|
|
24
|
+ |
to_state_version: 2,
|
|
25
|
+ |
artifact_bytes: artifact,
|
|
26
|
+ |
artifact_digest: sha256(artifact)
|
|
27
|
+ |
}
|
|
28
|
+ |
|
|
29
|
+ |
opts = [
|
|
30
|
+ |
release_root: root,
|
|
31
|
+ |
release_handler: ReleaseHandler,
|
|
32
|
+ |
generate_config: fn _version -> :ok end,
|
|
33
|
+ |
health: fn -> %{"ready" => true} end,
|
|
34
|
+ |
state: fn -> %{schema_version: 2} end
|
|
35
|
+ |
]
|
|
36
|
+ |
|
|
37
|
+ |
%{root: root, request: request, opts: opts}
|
|
38
|
+ |
end
|
|
39
|
+ |
|
|
40
|
+ |
test "restages the consumed artifact from immutable cache", %{
|
|
41
|
+ |
root: root,
|
|
42
|
+ |
request: request,
|
|
43
|
+ |
opts: opts
|
|
44
|
+ |
} do
|
|
45
|
+ |
assert {:ok, %{"phase" => "staged"}} = RelupNode.stage(request, opts)
|
|
46
|
+ |
assert {:ok, %{"phase" => "stage_verified"}} = RelupNode.verify_stage(request, opts)
|
|
47
|
+ |
assert {:ok, %{"phase" => "unpacked"}} = RelupNode.unpack(request, opts)
|
| 17 |
48
|
|
|
| 18 |
|
- |
artifact = "immutable release artifact"
|
| 19 |
|
- |
|
| 20 |
|
- |
request = %{
|
| 21 |
|
- |
release_name: "openagents",
|
| 22 |
|
- |
from_version: "0.1.0",
|
| 23 |
|
- |
to_version: "0.2.0",
|
| 24 |
|
- |
from_state_version: 1,
|
| 25 |
|
- |
to_state_version: 2,
|
| 26 |
|
- |
artifact_bytes: artifact,
|
| 27 |
|
- |
artifact_digest: sha256(artifact)
|
| 28 |
|
- |
}
|
| 29 |
|
- |
|
| 30 |
|
- |
opts = [
|
| 31 |
|
- |
release_root: root,
|
| 32 |
|
- |
release_handler: ReleaseHandler,
|
| 33 |
|
- |
generate_config: fn _version -> :ok end,
|
| 34 |
|
- |
health: fn -> %{"ready" => true} end,
|
| 35 |
|
- |
state: fn -> %{schema_version: 2} end
|
| 36 |
|
- |
]
|
| 37 |
|
- |
|
| 38 |
|
- |
%{root: root, request: request, opts: opts}
|
|
49
|
+ |
staged = Path.join([root, "releases", "openagents-0.2.0.tar.gz"])
|
|
50
|
+ |
File.rm!(staged)
|
|
51
|
+ |
|
|
52
|
+ |
assert {:ok, %{"phase" => "unpacked", "restaged" => true}} = RelupNode.unpack(request, opts)
|
|
53
|
+ |
assert File.read!(staged) == request.artifact_bytes
|
|
54
|
+ |
end
|
|
55
|
+ |
|
|
56
|
+ |
test "checks, installs, verifies, and makes the release permanent", %{
|
|
57
|
+ |
request: request,
|
|
58
|
+ |
opts: opts
|
|
59
|
+ |
} do
|
|
60
|
+ |
assert {:ok, _result} = RelupNode.stage(request, opts)
|
|
61
|
+ |
assert {:ok, _result} = RelupNode.unpack(request, opts)
|
|
62
|
+ |
assert {:ok, %{"phase" => "checked"}} = RelupNode.check_install(request, opts)
|
|
63
|
+ |
assert {:ok, %{"phase" => "installed"}} = RelupNode.install(request, opts)
|
|
64
|
+ |
assert {:ok, %{"phase" => "verified"}} = RelupNode.verify(request, :current, opts)
|
|
65
|
+ |
assert {:ok, %{"phase" => "permanent"}} = RelupNode.make_permanent(request, opts)
|
|
66
|
+ |
assert {:ok, %{"phase" => "verified"}} = RelupNode.verify(request, :permanent, opts)
|
|
67
|
+ |
end
|
|
68
|
+ |
|
|
69
|
+ |
test "refuses a candidate whose staged bytes are not the requested artifact", %{
|
|
70
|
+ |
request: request,
|
|
71
|
+ |
opts: opts
|
|
72
|
+ |
} do
|
|
73
|
+ |
assert {:error, :artifact_digest_mismatch} =
|
|
74
|
+ |
RelupNode.stage(%{request | artifact_bytes: "other bytes"}, opts)
|
|
75
|
+ |
end
|
|
76
|
+ |
|
|
77
|
+ |
test "refuses a version already unpacked from different bytes", %{
|
|
78
|
+ |
request: request,
|
|
79
|
+ |
opts: opts
|
|
80
|
+ |
} do
|
|
81
|
+ |
assert {:ok, _result} = RelupNode.stage(request, opts)
|
|
82
|
+ |
assert {:ok, _result} = RelupNode.unpack(request, opts)
|
|
83
|
+ |
|
|
84
|
+ |
recut = "0.2.0 rebuilt from another revision"
|
|
85
|
+ |
recut = %{request | artifact_bytes: recut, artifact_digest: sha256(recut)}
|
|
86
|
+ |
|
|
87
|
+ |
assert {:ok, _result} = RelupNode.stage(recut, opts)
|
|
88
|
+ |
assert {:error, :unpacked_version_conflict} = RelupNode.unpack(recut, opts)
|
|
89
|
+ |
end
|
| 39 |
90
|
|
end
|
| 40 |
91
|
|
|
| 41 |
|
- |
test "restages the consumed artifact from immutable cache", %{
|
| 42 |
|
- |
root: root,
|
| 43 |
|
- |
request: request,
|
| 44 |
|
- |
opts: opts
|
| 45 |
|
- |
} do
|
| 46 |
|
- |
assert {:ok, %{"phase" => "staged"}} = RelupNode.stage(request, opts)
|
| 47 |
|
- |
assert {:ok, %{"phase" => "stage_verified"}} = RelupNode.verify_stage(request, opts)
|
| 48 |
|
- |
assert {:ok, %{"phase" => "unpacked"}} = RelupNode.unpack(request, opts)
|
|
92
|
+ |
describe "a same-schema pair through forward, reverse, and forward again" do
|
|
93
|
+ |
setup do
|
|
94
|
+ |
state = start_supervised!({ReleaseState, name: nil})
|
|
95
|
+ |
:ok = ReleaseState.observe("retained", state)
|
|
96
|
+ |
|
|
97
|
+ |
# The instructions the appup emits for a 2 -> 2 pair. `AppupTest` proves
|
|
98
|
+ |
# the generator emits exactly these extras in each direction.
|
|
99
|
+ |
installer = fn
|
|
100
|
+ |
"0.3.0" -> migrate(state, ~c"0.2.0", schema_version: 2)
|
|
101
|
+ |
"0.2.0" -> migrate(state, {:down, ~c"0.3.0"}, schema_version: 2)
|
|
102
|
+ |
end
|
|
103
|
+ |
|
|
104
|
+ |
start_supervised!(
|
|
105
|
+ |
{ReleaseHandler,
|
|
106
|
+ |
%{
|
|
107
|
+ |
releases: [{~c"openagents", ~c"0.2.0", [], :permanent}],
|
|
108
|
+ |
pair: {"0.2.0", "0.3.0"},
|
|
109
|
+ |
on_install: installer
|
|
110
|
+ |
}}
|
|
111
|
+ |
)
|
| 49 |
112
|
|
|
| 50 |
|
- |
staged = Path.join([root, "releases", "openagents-0.2.0.tar.gz"])
|
| 51 |
|
- |
File.rm!(staged)
|
|
113
|
+ |
artifact = "immutable 0.3.0 artifact"
|
| 52 |
114
|
|
|
| 53 |
|
- |
assert {:ok, %{"phase" => "unpacked", "restaged" => true}} = RelupNode.unpack(request, opts)
|
| 54 |
|
- |
assert File.read!(staged) == request.artifact_bytes
|
|
115
|
+ |
request = %{
|
|
116
|
+ |
release_name: "openagents",
|
|
117
|
+ |
from_version: "0.2.0",
|
|
118
|
+ |
to_version: "0.3.0",
|
|
119
|
+ |
from_state_version: 2,
|
|
120
|
+ |
to_state_version: 2,
|
|
121
|
+ |
artifact_bytes: artifact,
|
|
122
|
+ |
artifact_digest: sha256(artifact)
|
|
123
|
+ |
}
|
|
124
|
+ |
|
|
125
|
+ |
opts = [
|
|
126
|
+ |
release_root: release_root(),
|
|
127
|
+ |
release_handler: ReleaseHandler,
|
|
128
|
+ |
generate_config: fn _version -> :ok end,
|
|
129
|
+ |
health: fn -> %{"ready" => true} end,
|
|
130
|
+ |
state: fn -> ReleaseState.snapshot(state) end
|
|
131
|
+ |
]
|
|
132
|
+ |
|
|
133
|
+ |
%{state: state, request: request, opts: opts}
|
|
134
|
+ |
end
|
|
135
|
+ |
|
|
136
|
+ |
test "verifies forward, reverses without corrupting state, and re-upgrades", %{
|
|
137
|
+ |
state: state,
|
|
138
|
+ |
request: request,
|
|
139
|
+ |
opts: opts
|
|
140
|
+ |
} do
|
|
141
|
+ |
assert {:ok, _result} = RelupNode.stage(request, opts)
|
|
142
|
+ |
assert {:ok, _result} = RelupNode.verify_stage(request, opts)
|
|
143
|
+ |
assert {:ok, _result} = RelupNode.unpack(request, opts)
|
|
144
|
+ |
assert {:ok, _result} = RelupNode.check_install(request, opts)
|
|
145
|
+ |
assert {:ok, _result} = RelupNode.install(request, opts)
|
|
146
|
+ |
assert {:ok, %{"phase" => "verified"}} = RelupNode.verify(request, :current, opts)
|
|
147
|
+ |
assert {:ok, _result} = RelupNode.make_permanent(request, opts)
|
|
148
|
+ |
assert {:ok, %{"phase" => "verified"}} = RelupNode.verify(request, :permanent, opts)
|
|
149
|
+ |
|
|
150
|
+ |
# `reverse/2` installs 0.2.0, checks health and schema, then restores
|
|
151
|
+ |
# permanence. Before the target schema became explicit this left the
|
|
152
|
+ |
# process on schema 1, so verify_reverse_health/2 failed with
|
|
153
|
+ |
# :reverse_state_version_mismatch and permanence was never restored.
|
|
154
|
+ |
assert {:ok, %{"phase" => "reversed", "restored" => true}} =
|
|
155
|
+ |
RelupNode.reverse(request, opts)
|
|
156
|
+ |
|
|
157
|
+ |
assert %State{schema_version: 2, observations: ["retained"], integrity: integrity} =
|
|
158
|
+ |
ReleaseState.snapshot(state)
|
|
159
|
+ |
|
|
160
|
+ |
assert is_binary(integrity)
|
|
161
|
+ |
|
|
162
|
+ |
assert {:ok, _result} = RelupNode.install(request, opts)
|
|
163
|
+ |
assert {:ok, %{"phase" => "verified"}} = RelupNode.verify(request, :current, opts)
|
|
164
|
+ |
assert {:ok, _result} = RelupNode.make_permanent(request, opts)
|
|
165
|
+ |
assert {:ok, %{"phase" => "verified"}} = RelupNode.verify(request, :permanent, opts)
|
|
166
|
+ |
|
|
167
|
+ |
assert %State{schema_version: 2, observations: ["retained"]} = ReleaseState.snapshot(state)
|
|
168
|
+ |
end
|
| 55 |
169
|
|
end
|
| 56 |
170
|
|
|
| 57 |
|
- |
test "checks, installs, verifies, and makes the release permanent", %{
|
| 58 |
|
- |
request: request,
|
| 59 |
|
- |
opts: opts
|
| 60 |
|
- |
} do
|
| 61 |
|
- |
assert {:ok, _result} = RelupNode.stage(request, opts)
|
| 62 |
|
- |
assert {:ok, _result} = RelupNode.unpack(request, opts)
|
| 63 |
|
- |
assert {:ok, %{"phase" => "checked"}} = RelupNode.check_install(request, opts)
|
| 64 |
|
- |
assert {:ok, %{"phase" => "installed"}} = RelupNode.install(request, opts)
|
| 65 |
|
- |
assert {:ok, %{"phase" => "verified"}} = RelupNode.verify(request, :current, opts)
|
| 66 |
|
- |
assert {:ok, %{"phase" => "permanent"}} = RelupNode.make_permanent(request, opts)
|
| 67 |
|
- |
assert {:ok, %{"phase" => "verified"}} = RelupNode.verify(request, :permanent, opts)
|
|
171
|
+ |
defp migrate(pid, version, extra) do
|
|
172
|
+ |
:ok = :sys.suspend(pid)
|
|
173
|
+ |
:ok = :sys.change_code(pid, ReleaseState, version, extra)
|
|
174
|
+ |
:ok = :sys.resume(pid)
|
|
175
|
+ |
end
|
|
176
|
+ |
|
|
177
|
+ |
defp release_root do
|
|
178
|
+ |
root =
|
|
179
|
+ |
Path.join(System.tmp_dir!(), "openagents-relup-node-#{System.unique_integer([:positive])}")
|
|
180
|
+ |
|
|
181
|
+ |
File.mkdir_p!(Path.join(root, "releases"))
|
|
182
|
+ |
on_exit(fn -> File.rm_rf!(root) end)
|
|
183
|
+ |
root
|
| 68 |
184
|
|
end
|
| 69 |
185
|
|
|
| 70 |
186
|
|
defp sha256(bytes), do: :crypto.hash(:sha256, bytes) |> Base.encode16(case: :lower)
|