carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "sync/atomic"
5
6 "github.com/carlosframework/platform/internal/instanceupdates"
7 "github.com/carlosframework/platform/internal/registry"
8 "github.com/carlosframework/platform/internal/release"
9 "github.com/carlosframework/platform/internal/status"
10)
11
12// buildStatesFailed is set by runAgent (noteBuildStatesInit) when the
13// build-state store did not start. The refusing BeforeExec hook is then
14// installed, so the box runs nothing it cannot record.
15var buildStatesFailed atomic.Bool
16
17// noteBuildStatesInit records how newBuildStates ended, for the report. A box
18// with no replica bucket is (nil, nil): it runs no hibernating instance, so
19// it has no build state to keep, and it enforces everything it has to; it
20// reports the protocol.
21func noteBuildStatesInit(err error) { buildStatesFailed.Store(err != nil) }
22
23// boxSettingsMetrics is this binary's settings protocol: it applies pin,
24// manual and major, and its retention protects the builds they name
25// (settings spec §4.4). It lifts the console's refusal. A box whose
26// build-state service failed to start reports 0: it refuses every exec and
27// enforces nothing (Astra I3 #1).
28func boxSettingsMetrics() int {
29 if buildStatesFailed.Load() {
30 return 0
31 }
32 return status.UpdateSettingsProtocol
33}
34
35// fillUpdateProbes adds the update-settings report to one status tick's
36// probes: the protocol this box enforces and root's last decision per host.
37func fillUpdateProbes(p *status.Probes, reg *registry.Registry, optDir string) {
38 p.UpdateSettings = boxSettingsMetrics()
39 p.Updates = updatesReport(reg, optDir)
40}
41
42// updatesReport is root's last decision per host with settings or a decision,
43// for the console's list and instance page, each naming the host's tenant. The column is read as root reads
44// it (ParseUpdateSettingsLenient): a Control name a newer console added must
45// not blank the report. A column that does not parse even then still reports
46// root's decision (its reason is unknown).
47func updatesReport(reg *registry.Registry, optDir string) map[string]status.HostUpdates {
48 routes, err := reg.List()
49 if err != nil {
50 return nil
51 }
52 out := map[string]status.HostUpdates{}
53 for _, rt := range routes {
54 u, perr := release.ParseUpdateSettingsLenient(rt.UpdateSettings)
55 c, _ := release.ReadConsumed(optDir, rt.Host)
56 // The consumed file is keyed by hostname only. One that does not name
57 // this route's tenant (a previous occupant's, or one from before root
58 // named the tenant) is no decision for this route: the entry carries
59 // the route's own column and nothing that came from the file or the
60 // build state's watermark (Astra r2 N1, ruling R22).
61 foreign := c != nil && !c.DecidedFor(rt)
62 if foreign {
63 c = nil
64 }
65 if perr == nil && u.Default() && c == nil {
66 continue
67 }
68 if perr != nil && c == nil {
69 continue
70 }
71 bs, _ := release.ParseBuildStateCopy(rt.BuildState)
72 // The tenant is the route's, by the rollout report's rule
73 // (HostRollout.Account and .App), so the console can attribute the
74 // report without a rollout report (Astra r1, R18).
75 h := status.HostUpdates{Account: release.RouteAccount(rt), App: rt.App}
76 if perr == nil {
77 h.Major = u.Major // the route's own column: its tenant's
78 }
79 if foreign {
80 // Completed and Pending read the watermark, which root takes
81 // from the file and the build state together (Watermark); with
82 // the file not this tenant's, neither is reported until root
83 // decides for this route.
84 out[rt.Host] = h
85 continue
86 }
87 if c != nil {
88 h.Desired, h.Reason, h.Detail, h.Baking = c.Desired, c.Reason, c.Detail, c.Baking
89 // Root writes a raw parse error as the detail of reason unknown.
90 // Members see only a build version.
91 if h.Reason == string(release.ReasonUnknown) && !release.ValidVersion(h.Detail) {
92 h.Detail = ""
93 }
94 h.Completed, h.CompletedFailed = c.Completed, c.CompletedFailed
95 }
96 // The build state joins the watermark only once the copy has been
97 // read; unread, root does no request bookkeeping either.
98 if bs.Known && bs.State != nil {
99 if w := release.Watermark(c, bs.State); w != h.Completed {
100 h.Completed, h.CompletedFailed = w, bs.State.CompletedFailed
101 }
102 }
103 if perr == nil && u.InstallRequest > h.Completed {
104 h.Pending = u.InstallRequest
105 }
106 out[rt.Host] = h
107 }
108 if len(out) == 0 {
109 return nil
110 }
111 return out
112}
113
114// updatesReader is the control socket's reader (plan 3's RegistryReader,
115// which keeps the channel fallback, plus root's decision and the serving
116// build). Root's decision is added only when it names the route's tenant,
117// as in updatesReport (ruling R22): an instance never reads a previous
118// occupant's decision for its hostname.
119func updatesReader(reg *registry.Registry, optDir string) func(string) (instanceupdates.Report, error) {
120 base := instanceupdates.RegistryReader(reg)
121 return func(host string) (instanceupdates.Report, error) {
122 rep, err := base(host)
123 if err != nil {
124 return rep, err
125 }
126 rt, err := reg.Get(host)
127 if err != nil || rt == nil {
128 return rep, nil // the base report stands; no decision is added
129 }
130 if c, _ := release.ReadConsumed(optDir, host); c.DecidedFor(*rt) {
131 rep.Desired, rep.Reason = c.Desired, c.Reason
132 }
133 return rep, nil
134 }
135}
136