carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package release
2
3import (
4 "encoding/json"
5 "fmt"
6 "slices"
7)
8
9// The update settings vocabulary (instance update settings spec §3).
10const (
11 UpdatesAuto = "auto"
12 UpdatesManual = "manual"
13
14 ControlUpdates = "updates"
15 ControlChannel = "channel"
16 ControlMajor = "major"
17 ControlPin = "pin"
18)
19
20// UpdateSettings are one instance's EFFECTIVE settings: the record's explicit
21// values over the app's defaults, resolved by reconcile
22// (provision.EffectiveUpdates) and carried to root in the registry's
23// update_settings column. A preference, not a security boundary (spec §2
24// decision 5): root still installs only builds §4.1 authorizes.
25type UpdateSettings struct {
26 Updates string `json:"updates"`
27 Channel string `json:"channel"`
28 Major *int `json:"major,omitempty"`
29 Pin string `json:"pin,omitempty"`
30 InstallRequest uint64 `json:"install_request,omitempty"`
31 InstallFastTrack bool `json:"install_fast_track,omitempty"`
32 Control []string `json:"control,omitempty"`
33}
34
35// ParseUpdateSettings reads the column. "" is every row before this feature,
36// and reads as auto: today's adoption, byte for byte.
37func ParseUpdateSettings(col string) (UpdateSettings, error) {
38 if col == "" {
39 return UpdateSettings{Updates: UpdatesAuto}, nil
40 }
41 var u UpdateSettings
42 if err := json.Unmarshal([]byte(col), &u); err != nil {
43 return UpdateSettings{}, fmt.Errorf("update settings: %v", err)
44 }
45 if u.Updates != UpdatesAuto && u.Updates != UpdatesManual {
46 return UpdateSettings{}, fmt.Errorf("update settings: updates %q", u.Updates)
47 }
48 if u.Major != nil && *u.Major < 0 {
49 return UpdateSettings{}, fmt.Errorf("update settings: major %d", *u.Major)
50 }
51 if u.Pin != "" && (!ValidVersion(u.Pin) || u.Pin == "dev") {
52 return UpdateSettings{}, fmt.Errorf("update settings: pin %q", u.Pin)
53 }
54 if u.Channel != "" && !ValidChannel(u.Channel) {
55 return UpdateSettings{}, fmt.Errorf("update settings: channel %q", u.Channel)
56 }
57 if !ValidControl(u.Control) {
58 return UpdateSettings{}, fmt.Errorf("update settings: control %q", u.Control)
59 }
60 return u, nil
61}
62
63// Column is the canonical JSON reconcile writes. Default settings are "":
64// a row is byte-identical to before this feature until someone sets
65// something.
66func (u UpdateSettings) Column() string {
67 if u.Default() && len(u.Control) == 0 {
68 return ""
69 }
70 b, _ := json.Marshal(u)
71 return string(b)
72}
73
74// Default reports today's behaviour: auto, no major hold, no pin, no request.
75func (u UpdateSettings) Default() bool {
76 return (u.Updates == "" || u.Updates == UpdatesAuto) && u.Major == nil && u.Pin == "" && u.InstallRequest == 0
77}
78
79// ValidControl accepts a set of setting names an instance may change itself.
80func ValidControl(names []string) bool {
81 seen := map[string]bool{}
82 for _, n := range names {
83 if seen[n] || !slices.Contains([]string{ControlUpdates, ControlChannel, ControlMajor, ControlPin}, n) {
84 return false
85 }
86 seen[n] = true
87 }
88 return true
89}
90
91// ParseUpdateSettingsLenient is the box's read of the column (root's pass,
92// the agent's passes and the held wake check): ParseUpdateSettings with
93// Control names this binary does not know, and repeats, dropped first.
94// Control is the console's business (who may change what); the box never
95// acts on it, so a name a newer console or policy added must not make the
96// whole column unreadable and drop the hold it carries (ruling
97// R-SETTINGS-UNREADABLE). Every other field is validated as strictly as
98// ParseUpdateSettings does; the console keeps the strict parse.
99func ParseUpdateSettingsLenient(col string) (UpdateSettings, error) {
100 if col == "" {
101 return ParseUpdateSettings(col)
102 }
103 var u UpdateSettings
104 if err := json.Unmarshal([]byte(col), &u); err != nil {
105 return UpdateSettings{}, fmt.Errorf("update settings: %v", err)
106 }
107 var known []string
108 for _, n := range u.Control {
109 if ValidControl([]string{n}) && !slices.Contains(known, n) {
110 known = append(known, n)
111 }
112 }
113 u.Control = known
114 b, err := json.Marshal(u)
115 if err != nil {
116 return UpdateSettings{}, fmt.Errorf("update settings: %v", err)
117 }
118 return ParseUpdateSettings(string(b))
119}
120