domain/architecture/system/effects.bend
near-v-framework@0.3.0.0 · 315 lines · 0 definitions
1import Base
2import bend-kit-files@0.1.0.0/files.bend as Fs
3import near-v-framework@0.3.0.0/domain/architecture/system/effs/io.bend as X
4import near-v-framework@0.3.0.0/domain/architecture/type.bend as A
5import near-v-framework@0.3.0.0/domain/architecture/ops.bend as O
6import near-v-framework@0.3.0.0/domain/architecture/system/container/effects.bend as CtE
7import near-v-framework@0.3.0.0/domain/architecture/system/deployment/type.bend as D
8import near-v-framework@0.3.0.0/domain/architecture/system/deployment/ops.bend as DO
9import near-v-framework@0.3.0.0/domain/architecture/system/deployment/azure/effects.bend as AzE
10import near-v-framework@0.3.0.0/domain/architecture/system/deployment/nix_darwin/type.bend as ND
11import near-v-framework@0.3.0.0/domain/architecture/system/deployment/nix_darwin/ops.bend as NDO
12import near-v-framework@0.3.0.0/domain/architecture/system/deployment/nix_darwin/effects.bend as NDE
13import near-v-framework@0.3.0.0/domain/architecture/system/deployment/nix_darwin/setting/type.bend as St
14import near-v-framework@0.3.0.0/domain/architecture/system/deployment/nix_darwin/setting/ops.bend as StO
15import near-v-framework@0.3.0.0/domain/architecture/system/deployment/nix_darwin/brew/type.bend as Br
16import near-v-framework@0.3.0.0/domain/architecture/system/deployment/nix_darwin/setting/effects.bend as StE
17import near-v-framework@0.3.0.0/domain/architecture/system/deployment/nix_darwin/brew/effects.bend as BrE
18import near-v-framework@0.3.0.0/domain/architecture/system/deployment/nix_darwin/service/ops.bend as SvO
19import near-v-framework@0.3.0.0/domain/architecture/system/release/type.bend as Rl
20import near-v-framework@0.3.0.0/domain/architecture/system/release/ops.bend as RlO
21import near-v-framework@0.3.0.0/domain/architecture/system/plan/type.bend as P
22import near-v-framework@0.3.0.0/domain/architecture/system/plan/ops.bend as PO
23import near-v-framework@0.3.0.0/domain/architecture/system/ops.bend as SO
24
25# The system's effects: reading the repository, the Bend hub and GitHub (a snapshot of the world), and
26# changing them (a deploy). Programs run with their arguments, never through a shell; files are read with
27# Base's File API.
28
29# ---- the repository's files ----
30def files(+root: String) -> IO(List<&2, String>):
31 do IO<List<&2, String>>:
32 out : String <- X.run("git", ["-C", root, "ls-files", "-co", "--exclude-standard"])
33 return X.nonempty(String.lines(out))
34
35# ---- what an entry reaches: it and every file it imports, followed in Bend ----
36def second_word(+t: String) -> String:
37 Maybe.default(&2, String, List.get(&2, String, X.nonempty(String.split(t, ' ')), 1n), "")
38
39def unquote(+w: String) -> String:
40 Bool.pick(String, String.starts_with(w, "\""), Maybe.default(&2, String, List.get(&2, String, String.split(w, '"'), 1n), ""), w)
41
42# The file a line imports, relative to the repository, or "" when it imports none of the repository's.
43def import_of(+line: String, +dir: String) -> String:
44 +w = unquote(second_word(String.trim(line)))
45 Bool.pick(String, String.starts_with(String.trim(line), "import ") && (String.starts_with(w, "./") || String.starts_with(w, "../")),
46 Fs.path.normalize(Fs.path.join(dir, w)), "")
47
48def imports(ls: List<&2, String>, +dir: String) -> List<&2, String>:
49 match ls:
50 case []:
51 []
52 case l <> rest:
53 X.nonempty(import_of(l, dir) <> imports(rest, dir))
54
55# Each step reads one file; the number of files squared bounds the walk.
56def reach(fuel: Nat, +todo: List<&2, String>, +seen: List<&2, String>, +root: String) -> IO(List<&2, String>):
57 match fuel:
58 case 0n:
59 IO.pure(List<&2, String>, seen)
60 case 1n+k:
61 match todo:
62 case []:
63 IO.pure(List<&2, String>, seen)
64 case +f <> rest:
65 do IO<List<&2, String>>:
66 text : String <- X.read_head(root ++ "/" ++ f, 4096)
67 +known : Bool = List.contains(~String, ~String.eq, seen, f)
68 reach(k, List.append(&2, String, rest, Bool.pick(List<&2, String>, known, [], imports(String.lines(text), Fs.path.parent(f)))), Bool.pick(List<&2, String>, known, seen, f <> seen), root)
69
70# ---- the Bend hub: has it published this package, with these exact files? ----
71def differing(fs: List<&2, String>, +local: String, +hub: String) -> IO(String):
72 match fs:
73 case []:
74 IO.pure(String, "")
75 case +f <> rest:
76 do IO<String>:
77 same : Bool <- X.succeeds("cmp", ["-s", local ++ "/" ++ f, hub ++ "/" ++ f])
78 more : String <- differing(rest, local, hub)
79 return Bool.pick(String, same, more, f)
80
81def hub_of(+published: String, +f: String) -> P.Hub:
82 Bool.pick(P.Hub, String.is_empty(published), P.Unpublished{}, Bool.pick(P.Hub, String.is_empty(f), P.Same{}, P.Differs{f}))
83
84def hub_state(+home: String, +root: String, p: Rl.Package) -> IO(P.Hub):
85 match p:
86 case Rl.Package{n, v, +e, _}:
87 do IO<P.Hub>:
88 +hash : String <- X.read(home ++ "/.bend/lib/names/" ++ n ++ "@" ++ v)
89 +files : List<&2, String> <- reach(1000n, [e], [], root)
90 f : String <- differing(files, root, home ++ "/.bend/lib/" ++ String.trim(hash))
91 return hub_of(String.trim(hash), f)
92
93def hub_states(+home: String, +root: String, ps: List<&2, Rl.Package>) -> IO(List<&2, P.Hub>):
94 match ps:
95 case []:
96 IO.pure(List<&2, P.Hub>, [])
97 case p <> rest:
98 do IO<List<&2, P.Hub>>:
99 h : P.Hub <- hub_state(home, root, p)
100 hs : List<&2, P.Hub> <- hub_states(home, root, rest)
101 return h <> hs
102
103# ---- GitHub ----
104def first_word(+t: String) -> String:
105 Maybe.default(&2, String, List.head(&2, String, X.nonempty(String.split(t, '\t'))), "")
106
107def pushed(+root: String, +sha: String) -> IO(Bool):
108 Bool.pick(IO(Bool), String.is_empty(sha), IO.pure(Bool, False{}), X.succeeds("git", ["-C", root, "merge-base", "--is-ancestor", "HEAD", sha]))
109
110def remote(+root: String, r: Rl.Repository) -> IO(P.Remote):
111 match r:
112 case Rl.Repository{+name, _}:
113 do IO<P.Remote>:
114 origin : String <- X.run("git", ["-C", root, "remote", "get-url", "origin"])
115 vis : String <- X.run("gh", ["repo", "view", name, "--json", "visibility", "--jq", ".visibility"])
116 status : String <- X.run("git", ["-C", root, "status", "--porcelain"])
117 head : String <- X.run("git", ["-C", root, "ls-remote", "origin", "refs/heads/main"])
118 up : Bool <- pushed(root, first_word(head))
119 return P.Remote{String.trim(origin), String.to_lower(String.trim(vis)), String.is_empty(String.trim(status)), up}
120
121# ---- a snapshot: everything a plan needs ----
122def root() -> IO(String):
123 do IO<String>:
124 t : String <- X.run("git", ["rev-parse", "--show-toplevel"])
125 return String.trim(t)
126
127# The files, what the packages' entries and the system's own entry reach, the hub, and GitHub.
128def observe(+root: String, +entry: String, r: Rl.Release) -> IO(P.Observed):
129 match r:
130 case Rl.Release{repo, +ps}:
131 do IO<P.Observed>:
132 +h : String <- X.home()
133 +fs : List<&2, String> <- files(root)
134 reached : List<&2, String> <- reach(Nat.mul(1n+List.length(&2, String, fs), 1n+List.length(&2, String, fs)), entry <> RlO.entries(ps), [], root)
135 hs : List<&2, P.Hub> <- hub_states(h, root, ps)
136 rm : P.Remote <- remote(root, repo)
137 return P.Observed{fs, reached, hs, rm}
138
139# ---- a deploy: carry out a plan with nothing blocked ----
140def apply(+root: String, +bend: String, s: P.Step) -> IO(Unit):
141 match s:
142 case P.Current{_}:
143 IO.print(PO.show(s))
144 case P.Publish{+n, +v, +e}:
145 do IO<Unit>:
146 out : String <- X.run(bend, [root ++ "/" ++ e, "--publish", n ++ "@" ++ v])
147 IO.print(Bool.pick(String, String.contains(out, "published"), "published " ++ n ++ "@" ++ v, "failed publish " ++ n ++ "@" ++ v))
148 case P.Push{+r}:
149 do IO<Unit>:
150 ok : Bool <- X.succeeds("git", ["-C", root, "push", "-q", "origin", "HEAD:main"])
151 IO.print(Bool.pick(String, ok, "pushed github " ++ r, "failed push github " ++ r))
152 case P.Rebuild{+h, +fl, +mac}:
153 do IO<Unit>:
154 _ : Result<&1, &1, U32 & String, Unit> <- Fs.mkdir(root ++ "/nix")
155 X.write(root ++ "/nix/flake.nix", fl)
156 X.write(root ++ "/nix/mac.nix", mac)
157 ok : Bool <- X.succeeds("sudo", ["-n", "/run/current-system/sw/bin/darwin-rebuild", "switch", "--flake", root ++ "/nix#" ++ h])
158 IO.print(Bool.pick(String, ok, "rebuilt nix-darwin " ++ h, "failed darwin-rebuild switch " ++ h))
159 case P.ServePort{+p, +t, ts}:
160 do IO<Unit>:
161 ok : Bool <- X.succeeds(ts, ["serve", "--bg", "--yes", "--https=" ++ U32.show(p), t])
162 IO.print(Bool.pick(String, ok, "served :" ++ U32.show(p) ++ " -> " ++ t, "failed serve :" ++ U32.show(p)))
163 case P.Change{_, _, _}:
164 IO.pure(Unit, Unit{})
165 case P.Blocked{_, _}:
166 IO.print(PO.show(s))
167
168def apply_all(+root: String, +bend: String, ss: List<&2, P.Step>) -> IO(Unit):
169 match ss:
170 case []:
171 IO.pure(Unit, Unit{})
172 case s <> rest:
173 do IO<Unit>:
174 apply(root, bend, s)
175 apply_all(root, bend, rest)
176
177def deploy_if(blocked: Bool, +root: String, +ss: List<&2, P.Step>) -> IO(Unit):
178 match blocked:
179 case True{}:
180 IO.die(Unit, 1, PO.show_all(ss) ++ "nothing deployed: the plan is blocked")
181 case False{}:
182 do IO<Unit>:
183 +h : String <- X.home()
184 apply_all(root, h ++ "/.bend/bin/bend", ss)
185
186def deploy(+root: String, +ss: List<&2, P.Step>) -> IO(Unit):
187 deploy_if(PO.any_blocked(ss), root, ss)
188
189# ---- the whole plan: the release, the stack, each deployment node, and what the deploy would break ----
190# ---- a nix-darwin node: its checks, what a rebuild would change, and the rebuild when anything would ----
191def has_change(ss: List<&2, P.Step>) -> Bool:
192 match ss:
193 case []:
194 False{}
195 case s <> rest:
196 match s:
197 case P.Change{_, _, _}:
198 True{}
199 case _:
200 has_change(rest)
201
202def mac_of(nd: ND.NixDarwin) -> String:
203 match nd:
204 case ND.NixDarwin{_, _, _, _, _, st, fw, br, _, _, _, _, _, _}:
205 StO.mac_nix(st, fw, br)
206
207def host_of(nd: ND.NixDarwin) -> String:
208 match nd:
209 case ND.NixDarwin{h, _, _, _, _, _, _, _, _, _, _, _, _, _}:
210 h
211
212def rebuild_if(c: Bool, +h: String, +fl: String, +mac: String) -> List<&2, P.Step>:
213 match c:
214 case True{}:
215 [P.Rebuild{h, fl, mac}]
216 case False{}:
217 [P.Current{"nix-darwin " ++ h}]
218
219# Whether a generated file differs from the one in the repository (compared by cmp, not in memory).
220def differs(+path: String, +text: String) -> IO(Bool):
221 do IO<Bool>:
222 X.write("/tmp/v-generated", text)
223 same : Bool <- X.succeeds("cmp", ["-s", "/tmp/v-generated", path])
224 return Bool.not(same)
225
226def darwin_steps(+root: String, +k: String, +nd: ND.NixDarwin) -> IO(List<&2, P.Step>):
227 match nd:
228 case ND.NixDarwin{_, _, _, _, _, +st, +fw, +br, _, _, _, _, _, _}:
229 do IO<List<&2, P.Step>>:
230 checks : List<&2, P.Step> <- NDE.checks(k, nd)
231 +settings : List<&2, P.Step> <- StE.changes(st, fw)
232 +brews : List<&2, P.Step> <- BrE.changes(br)
233 flake_new : Bool <- differs(root ++ "/nix/flake.nix", NDO.flake(nd))
234 mac_new : Bool <- differs(root ++ "/nix/mac.nix", mac_of(nd))
235 return List.append(&2, P.Step, checks, List.append(&2, P.Step, settings, List.append(&2, P.Step, brews,
236 rebuild_if(flake_new || mac_new || has_change(settings) || has_change(brews), host_of(nd), NDO.flake(nd), mac_of(nd)))))
237
238def platform_steps(p: D.Platform, +root: String, +k: String, +a: A.Architecture) -> IO(List<&2, P.Step>):
239 match p:
240 case D.Unmanaged{}:
241 IO.pure(List<&2, P.Step>, [])
242 case D.Darwin{+nd}:
243 darwin_steps(root, k, nd)
244 case D.AzureCloud{az}:
245 AzE.checks(az, O.agreement(a))
246
247def node_steps(ns: List<&2, D.Node>, +root: String, +a: A.Architecture) -> IO(List<&2, P.Step>):
248 match ns:
249 case []:
250 IO.pure(List<&2, P.Step>, [])
251 case n <> rest:
252 match n:
253 case D.Node{+k, _, _, _, _, pf, kids, _}:
254 do IO<List<&2, P.Step>>:
255 here : List<&2, P.Step> <- platform_steps(pf, root, k, a)
256 below : List<&2, P.Step> <- node_steps(kids, root, a)
257 after : List<&2, P.Step> <- node_steps(rest, root, a)
258 return List.append(&2, P.Step, here, List.append(&2, P.Step, below, after))
259
260def deployed_path(+root: String) -> String:
261 root ++ "/.git/v-deployed"
262
263def breaks_step(+bs: List<&2, D.Break>, +d: D.Deployment) -> List<&2, P.Step>:
264 Bool.pick(List<&2, P.Step>, DO.breaks_eq(bs, DO.accepted(d)), [],
265 [P.Blocked{"deployment", DO.show_breaks(bs) ++ " (declare it as accepted to deploy)"}])
266
267# What the deploy would break, against what this repository last deployed (recorded in .git, never committed).
268def deployed_steps(+root: String, +d: D.Deployment) -> IO(List<&2, P.Step>):
269 do IO<List<&2, P.Step>>:
270 +text : String <- X.read(deployed_path(root))
271 return breaks_step(DO.breaks(DO.recorded_address(text), DO.recorded_stores(text), d), d)
272
273def plan_all(+root: String, +entry: String, +a: A.Architecture) -> IO(List<&2, P.Step>):
274 do IO<List<&2, P.Step>>:
275 o : P.Observed <- observe(root, entry, SO.release(O.system(a)))
276 stack : List<&2, P.Step> <- CtE.checks(root, O.containers(a), O.externals(a))
277 nodes : List<&2, P.Step> <- node_steps(O.nodes(a), root, a)
278 breaks : List<&2, P.Step> <- deployed_steps(root, O.deployment(a))
279 return List.append(&2, P.Step, stack, List.append(&2, P.Step, nodes, List.append(&2, P.Step, breaks, PO.plan(SO.release(O.system(a)), o))))
280
281# A deploy of the whole plan; afterwards it records the deployment, so the next plan compares against it.
282def deploy_all(+root: String, +entry: String, +a: A.Architecture) -> IO(Unit):
283 do IO<Unit>:
284 +steps : List<&2, P.Step> <- plan_all(root, entry, a)
285 deploy(root, steps)
286 X.write(deployed_path(root), DO.record(O.deployment(a)))
287
288# ---- import: each nix-darwin node's Mac as it is now, as Bend to declare it with ----
289def import_node(p: D.Platform, +root: String) -> IO(Unit):
290 match p:
291 case D.Darwin{nd}:
292 match nd:
293 case ND.NixDarwin{h, _, _, _, _, _, _, _, svs, _, _, _, _, _}:
294 do IO<Unit>:
295 st : List<&2, St.Setting> <- StE.observed(root ++ "/nix/options.tsv", SvO.labels(svs))
296 fw : St.Firewall <- StE.firewall_now()
297 br : List<&2, Br.Brew> <- BrE.observed()
298 IO.print("# " ++ h ++ "\n" ++ StO.import_text(st, fw, br))
299 case _:
300 IO.pure(Unit, Unit{})
301
302def import_nodes(ns: List<&2, D.Node>, +root: String) -> IO(Unit):
303 match ns:
304 case []:
305 IO.pure(Unit, Unit{})
306 case n <> rest:
307 match n:
308 case D.Node{_, _, _, _, _, pf, kids, _}:
309 do IO<Unit>:
310 import_node(pf, root)
311 import_nodes(kids, root)
312 import_nodes(rest, root)
313
314def import_all(+root: String, a: A.Architecture) -> IO(Unit):
315 import_nodes(O.nodes(a), root)