package moves import ( "context" "encoding/json" "errors" "fmt" "os" "strings" "time" "github.com/carlosframework/platform/internal/release" ) // ProtocolVersion is the move protocol version this build speaks. const ProtocolVersion = 1 // FloorKey is where the operator-published protocol floor lives in the // release bucket. const FloorKey = "control/moves-protocol.json" // ConsoleKey is where a console with the moves switch on publishes the // protocol it speaks and the fingerprint of its secrets public key, once at // each start (replacing the last start's record). const ConsoleKey = "control/moves-console.json" // ConsoleRecord is the body at ConsoleKey. type ConsoleRecord struct { V int `json:"v"` Protocol int `json:"protocol"` SecretsKeyFP string `json:"secrets_key_fp,omitempty"` At time.Time `json:"at"` } // The box's local copy of the floor and its verdict on carlos.prev (spec // ยง11). The units that pick the executable at recovery read two of these // by fixed path (internal/host/units/carlos-system-*.service), so they are // constants and never configuration. const ( // FloorFile is the floor as JSON, relative to the box's DataDir // (/.moves/floor.json). Root's apply and the agent write it; // root's activation check reads it and nothing else, so it works offline // at boot. FloorFile = ".moves/floor.json" // FloorMinReleaseFile is the floor's min_release alone, one line, // relative to the box's DataDir (/.moves/floor.min-release). // The units compare it byte for byte with PrevAllowedFile. FloorMinReleaseFile = ".moves/floor.min-release" // PrevVersionFile is carlos.prev's version, one line, beside carlos.prev // in the box's opt dir (/opt/carlos/carlos.prev.version). PrevVersionFile = "carlos.prev.version" // PrevAllowedFile is the verdict on carlos.prev, beside it in the opt // dir: the min_release it was judged at or above, one line, in the same // bytes as FloorMinReleaseFile. Absent means carlos.prev may not run // while a floor is armed. PrevAllowedFile = "carlos.prev.allowed" ) // Floor is the minimum protocol version, and the first release that speaks // it, that the door requires of a host before a move may involve it. type Floor struct { Min int `json:"min"` MinRelease string `json:"min_release"` } // ReadFloor reads the floor. An absent object is (nil, nil): no floor has // been published. A present but unreadable one is an error, never a // silent "no floor". func ReadFloor(ctx context.Context, s release.Store) (*Floor, error) { body, _, err := s.Get(ctx, FloorKey) if errors.Is(err, os.ErrNotExist) { return nil, nil } if err != nil { return nil, fmt.Errorf("moves: read floor: %w", err) } var f Floor if err := json.Unmarshal(body, &f); err != nil { return nil, fmt.Errorf("moves: parse floor: %w", err) } return &f, nil } // ReleaseAtOrAbove reports whether version is at or above minRelease, both // "vMAJOR.MINOR.PATCH". It fails closed: a version that does not parse // (prereleases included) or a malformed minRelease is false with a non-nil // error. func ReleaseAtOrAbove(version, minRelease string) (bool, error) { min, ok := release.ParseSemVer(strings.TrimPrefix(minRelease, "v")) if !ok { return false, fmt.Errorf("moves: malformed minimum release %q", minRelease) } v, ok := release.ParseSemVer(strings.TrimPrefix(version, "v")) if !ok { return false, fmt.Errorf("moves: unparseable release %q", version) } return v.Compare(min) >= 0, nil }