carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package hostfiles
2
3import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "errors"
8 "fmt"
9 "os"
10
11 "github.com/carlosframework/platform/internal/release"
12)
13
14// This file holds the entry points other packages call: region HA's claim
15// (CopyManifest, at a new generation's admission) and moves (Publish, at
16// install). Both are dormant: nothing in a production path calls them yet.
17
18// ErrManifestExists means CopyManifest's target already holds a different
19// manifest. The copy wrote nothing.
20var ErrManifestExists = errors.New("hostfiles: the target generation already has a manifest")
21
22// ErrSourceMoved means CopyManifest's source is not at the Seq the caller
23// expected. The copy wrote nothing.
24var ErrSourceMoved = errors.New("hostfiles: the source manifest is not at the expected seq")
25
26// CopyManifest establishes toPrefix's manifest as an exact copy of
27// fromPrefix's: the same sequence number and the same entries, each still
28// naming its object by its origin-qualified key. No object is copied; the
29// new generation's publisher continues the sequence from the copy and
30// writes new objects under toPrefix only.
31//
32// origins are the prefixes the source's entries may reference, as for
33// Restore (nil or empty: fromPrefix alone). A source built by earlier
34// copies references earlier generations, so its caller passes the chain
35// its pin admits. Every entry is validated before anything is written; an
36// absent source is os.ErrNotExist, never an empty copy.
37//
38// expectSeq, when not 0, is the source revision the caller admitted. A
39// source at any other Seq is ErrSourceMoved and nothing is written, on a
40// first attempt or a retry. 0 copies whatever the source holds.
41//
42// The write is create-only. A target that already holds exactly the
43// manifest this call would write (a repeat whose first response was lost)
44// is success; any other target is ErrManifestExists and is left as it was.
45// That retry-safety holds only while the source cannot commit between
46// attempts: the source generation must be fenced or frozen, as the claim's
47// admission makes it. Against a live source a retry may find the target
48// holding an older revision and report ErrManifestExists; with expectSeq
49// set it reports ErrSourceMoved instead, and never copies a revision the
50// caller did not admit.
51func CopyManifest(ctx context.Context, s release.Store, fromPrefix, toPrefix string, expectSeq int64, origins []string) (Manifest, error) {
52 if err := checkPrefix(fromPrefix); err != nil {
53 return Manifest{}, err
54 }
55 if err := checkPrefix(toPrefix); err != nil {
56 return Manifest{}, err
57 }
58 if fromPrefix == toPrefix {
59 return Manifest{}, fmt.Errorf("hostfiles: copy %s onto itself", fromPrefix)
60 }
61 origins, err := admitted(fromPrefix, origins)
62 if err != nil {
63 return Manifest{}, err
64 }
65 m, _, found, err := load(ctx, s, fromPrefix)
66 if err != nil {
67 return Manifest{}, err
68 }
69 if !found {
70 return Manifest{}, fmt.Errorf("hostfiles: copy from %s: %w", fromPrefix, os.ErrNotExist)
71 }
72 if expectSeq != 0 && m.Seq != expectSeq {
73 return Manifest{}, fmt.Errorf("%w: %s is at seq %d, not %d", ErrSourceMoved, fromPrefix, m.Seq, expectSeq)
74 }
75 if err := checkManifest(m, origins); err != nil {
76 return Manifest{}, err
77 }
78 body, err := json.Marshal(m)
79 if err != nil {
80 return Manifest{}, fmt.Errorf("hostfiles: marshal manifest: %w", err)
81 }
82 if _, err := s.Put(ctx, ManifestKey(toPrefix), body, ""); errors.Is(err, release.ErrCASConflict) {
83 cur, _, gerr := s.Get(ctx, ManifestKey(toPrefix))
84 if gerr != nil {
85 return Manifest{}, gerr
86 }
87 if !bytes.Equal(cur, body) {
88 return Manifest{}, fmt.Errorf("%w: %s", ErrManifestExists, toPrefix)
89 }
90 } else if err != nil {
91 return Manifest{}, err
92 }
93 return m, nil
94}
95
96// Publish records dir as prefix's manifest in one revision and returns
97// that revision's sequence number: one SyncDir and one Commit on a fresh
98// Publisher. It is the call moves makes at install, after the directory is
99// in place and before the installation is recorded.
100//
101// skip is SyncDir's: it excludes a relative path and, for a directory,
102// everything under it. On success every regular file in dir that skip does
103// not exclude is in the manifest: published, or recorded as skipped when
104// it is larger than MaxFileBytes (st.TooLarge). Anything that is not a
105// regular file is left out and listed in st.NotRegular.
106//
107// Publish is an install-time contract, so it commits a complete and
108// consistent image or nothing. It returns an error, and commits nothing,
109// when any file could not be read (st.Unreadable), was replaced between
110// the look and the open (st.Retry), or changed while it was read
111// (st.Torn). The manifest would otherwise lack the file or hold bytes that
112// were never the file's state. The caller stops writers and calls Publish
113// again. Objects uploaded by a refused attempt are referenced by no
114// manifest.
115//
116// A prefix that already has a manifest is continued, not replaced: a
117// repeat with nothing changed returns the same sequence number. Another
118// writer's manifest appearing between Open and Commit is ErrSuperseded.
119// Platform items (wake state, an env file) are not part of this call; a
120// caller that needs them in the same revision uses Open, StagePlatform,
121// SyncDir and Commit itself.
122func Publish(ctx context.Context, s release.Store, prefix, dir string, skip func(rel string, isDir bool) bool) (int64, SyncStats, error) {
123 p, err := Open(ctx, s, prefix)
124 if err != nil {
125 return 0, SyncStats{}, err
126 }
127 st, err := SyncDir(ctx, p, dir, skip)
128 if err != nil {
129 return 0, st, err
130 }
131 if len(st.Unreadable) > 0 {
132 return 0, st, fmt.Errorf("hostfiles: publish %s: %d file(s) could not be read: %q", dir, len(st.Unreadable), st.Unreadable)
133 }
134 if len(st.Retry) > 0 {
135 return 0, st, fmt.Errorf("hostfiles: publish %s: %d file(s) were replaced while being looked at: %q", dir, len(st.Retry), st.Retry)
136 }
137 if len(st.Torn) > 0 {
138 return 0, st, fmt.Errorf("hostfiles: publish %s: %d file(s) changed while being read: %q", dir, len(st.Torn), st.Torn)
139 }
140 seq, err := p.Commit(ctx)
141 return seq, st, err
142}
143