carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package moves
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "os"
9 "strings"
10 "time"
11
12 "github.com/carlosframework/platform/internal/release"
13)
14
15// ProtocolVersion is the move protocol version this build speaks.
16const ProtocolVersion = 1
17
18// FloorKey is where the operator-published protocol floor lives in the
19// release bucket.
20const FloorKey = "control/moves-protocol.json"
21
22// ConsoleKey is where a console with the moves switch on publishes the
23// protocol it speaks and the fingerprint of its secrets public key, once at
24// each start (replacing the last start's record).
25const ConsoleKey = "control/moves-console.json"
26
27// ConsoleRecord is the body at ConsoleKey.
28type ConsoleRecord struct {
29 V int `json:"v"`
30 Protocol int `json:"protocol"`
31 SecretsKeyFP string `json:"secrets_key_fp,omitempty"`
32 At time.Time `json:"at"`
33}
34
35// The box's local copy of the floor and its verdict on carlos.prev (spec
36// §11). The units that pick the executable at recovery read two of these
37// by fixed path (internal/host/units/carlos-system-*.service), so they are
38// constants and never configuration.
39const (
40 // FloorFile is the floor as JSON, relative to the box's DataDir
41 // (<DataDir>/.moves/floor.json). Root's apply and the agent write it;
42 // root's activation check reads it and nothing else, so it works offline
43 // at boot.
44 FloorFile = ".moves/floor.json"
45 // FloorMinReleaseFile is the floor's min_release alone, one line,
46 // relative to the box's DataDir (<DataDir>/.moves/floor.min-release).
47 // The units compare it byte for byte with PrevAllowedFile.
48 FloorMinReleaseFile = ".moves/floor.min-release"
49 // PrevVersionFile is carlos.prev's version, one line, beside carlos.prev
50 // in the box's opt dir (/opt/carlos/carlos.prev.version).
51 PrevVersionFile = "carlos.prev.version"
52 // PrevAllowedFile is the verdict on carlos.prev, beside it in the opt
53 // dir: the min_release it was judged at or above, one line, in the same
54 // bytes as FloorMinReleaseFile. Absent means carlos.prev may not run
55 // while a floor is armed.
56 PrevAllowedFile = "carlos.prev.allowed"
57)
58
59// Floor is the minimum protocol version, and the first release that speaks
60// it, that the door requires of a host before a move may involve it.
61type Floor struct {
62 Min int `json:"min"`
63 MinRelease string `json:"min_release"`
64}
65
66// ReadFloor reads the floor. An absent object is (nil, nil): no floor has
67// been published. A present but unreadable one is an error, never a
68// silent "no floor".
69func ReadFloor(ctx context.Context, s release.Store) (*Floor, error) {
70 body, _, err := s.Get(ctx, FloorKey)
71 if errors.Is(err, os.ErrNotExist) {
72 return nil, nil
73 }
74 if err != nil {
75 return nil, fmt.Errorf("moves: read floor: %w", err)
76 }
77 var f Floor
78 if err := json.Unmarshal(body, &f); err != nil {
79 return nil, fmt.Errorf("moves: parse floor: %w", err)
80 }
81 return &f, nil
82}
83
84// ReleaseAtOrAbove reports whether version is at or above minRelease, both
85// "vMAJOR.MINOR.PATCH". It fails closed: a version that does not parse
86// (prereleases included) or a malformed minRelease is false with a non-nil
87// error.
88func ReleaseAtOrAbove(version, minRelease string) (bool, error) {
89 min, ok := release.ParseSemVer(strings.TrimPrefix(minRelease, "v"))
90 if !ok {
91 return false, fmt.Errorf("moves: malformed minimum release %q", minRelease)
92 }
93 v, ok := release.ParseSemVer(strings.TrimPrefix(version, "v"))
94 if !ok {
95 return false, fmt.Errorf("moves: unparseable release %q", version)
96 }
97 return v.Compare(min) >= 0, nil
98}
99