carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package activator
2
3import (
4 "errors"
5 "fmt"
6 "os"
7 "path/filepath"
8 "strings"
9
10 "github.com/carlosframework/platform/internal/instancefs"
11 "github.com/carlosframework/platform/internal/instancekey"
12 "github.com/carlosframework/platform/internal/registry"
13)
14
15// An import and the instance key (instancekey.go; merge review S1).
16//
17// The platform mints CARLOS_INSTANCE_KEY into the instance's own
18// <host>.env at its first launch, create-only, and a key there outranks one
19// the app's configuration sets (instancekey.Effective). An import's first
20// launch is its verify request, so an import that is then aborted has left
21// a key behind. Left in place it would outlive the rollback: a second
22// import of the same host, whose owner configured the old process's key
23// before verifying, would still be handed the first import's key, and the
24// values the old process sealed would not open.
25//
26// So the swap records what the own env file held before the import
27// (SeedEnvBefore, kept in the import journal), and the fenced rollback
28// retires what the import created and nothing else (RetireSeedKey).
29//
30// Removing a minted key is within the key's rules. Create-only means a key
31// is never replaced under data sealed with it. Here the data goes too: the
32// rollback removes the import's database and deletes its replica, under the
33// same fence. <host>.env is an installation artifact, retired with its
34// installation, as a move retires it on the box it leaves.
35
36// seedEnvDir is the directory holding the own env file of the nested row
37// whose database is dbPath, and the file's name.
38func seedEnvDir(dbPath, host string) (dir, name string, err error) {
39 dir, _, err = splitDBPath(dbPath)
40 if err != nil {
41 return "", "", err
42 }
43 if host == "" || host != filepath.Base(host) {
44 return "", "", fmt.Errorf("activator: %q is not a host name", host)
45 }
46 return dir, host + ".env", nil
47}
48
49// SeedEnvBefore reports what the own env file of the nested row at dbPath
50// holds now, as one of registry's EnvBefore values: no file, a file with no
51// instance key, or a file that carries one. The swap calls it before the
52// import's row exists, so before anything can launch the import. The file
53// is read through instancefs, as every nested read of it is. A file that
54// cannot be read (a link, a FIFO, over the limit) is an error: the caller
55// cannot tell, and does not swap.
56func SeedEnvBefore(dbPath, host string) (string, error) {
57 dir, name, err := seedEnvDir(dbPath, host)
58 if err != nil {
59 return "", err
60 }
61 adminTokenMu.Lock()
62 defer adminTokenMu.Unlock()
63 raw, exists, err := readSeedEnv(dir, name)
64 switch {
65 case err != nil:
66 return "", err
67 case !exists:
68 return registry.EnvBeforeAbsent, nil
69 case parseEnvBytes(raw)[InstanceKeyVar] != "":
70 return registry.EnvBeforeKey, nil
71 }
72 return registry.EnvBeforeNoKey, nil
73}
74
75// readSeedEnv reads dir/name beneath dir. An instance directory that is not
76// there holds no file.
77func readSeedEnv(dir, name string) (raw []byte, exists bool, err error) {
78 if _, err := os.Lstat(dir); errors.Is(err, os.ErrNotExist) {
79 return nil, false, nil
80 }
81 raw, err = instancefs.ReadBeneathLimit(dir, name, ownEnvReadLimit)
82 if errors.Is(err, os.ErrNotExist) {
83 return nil, false, nil
84 }
85 if err != nil {
86 return nil, false, fmt.Errorf("read %s: %w", filepath.Join(dir, name), err)
87 }
88 return raw, true, nil
89}
90
91// RetireSeedKey retires the instance key an import created, for the nested
92// row at dbPath, by what the swap recorded (before):
93//
94// - EnvBeforeAbsent: there was no env file, so the one there now is the
95// import's whole. It is removed, and with it the adoption record beside
96// it, which belongs to the same installation.
97// - EnvBeforeNoKey: the file was there with no key. Its CARLOS_INSTANCE_KEY
98// lines are the import's and are removed; every other line stays, byte
99// for byte.
100// - EnvBeforeKey: the key was there before the import. Nothing is touched.
101// - "": the journal row predates the record. Nothing is known, and nothing
102// is touched.
103//
104// The caller holds the host fenced and stopped (the import rollback's step
105// 3), so no launch reads the file meanwhile; adminTokenMu keeps the
106// startup backfill's append out. It is idempotent: a second run finds
107// nothing of the import's left.
108func RetireSeedKey(dbPath, host, before string) error {
109 switch before {
110 case registry.EnvBeforeAbsent, registry.EnvBeforeNoKey:
111 case registry.EnvBeforeKey, "":
112 return nil
113 default:
114 return fmt.Errorf("activator: %s: %q is not a recorded env state", host, before)
115 }
116 dir, name, err := seedEnvDir(dbPath, host)
117 if err != nil {
118 return err
119 }
120 adminTokenMu.Lock()
121 defer adminTokenMu.Unlock()
122 if _, err := os.Lstat(dir); errors.Is(err, os.ErrNotExist) {
123 return nil
124 }
125 if before == registry.EnvBeforeAbsent {
126 for _, n := range []string{name, host + instancekey.AdoptionSuffix} {
127 if err := instancefs.RemoveAllBeneath(dir, n); err != nil {
128 return fmt.Errorf("retire %s: %w", filepath.Join(dir, n), err)
129 }
130 }
131 return syncDir(dir)
132 }
133 raw, exists, err := readSeedEnv(dir, name)
134 if err != nil || !exists {
135 return err
136 }
137 kept, removed := withoutInstanceKey(raw)
138 if !removed {
139 return nil
140 }
141 if err := instancefs.WriteAtomic(dir, name, kept, 0o600); err != nil {
142 return fmt.Errorf("retire the instance key in %s: %w", filepath.Join(dir, name), err)
143 }
144 return syncDir(dir)
145}
146
147// withoutInstanceKey is raw with every line that sets CARLOS_INSTANCE_KEY
148// removed, by instancekey.ParseEnv's reading of a line; the other lines are
149// kept exactly. removed is whether any line went.
150func withoutInstanceKey(raw []byte) (kept []byte, removed bool) {
151 lines := strings.Split(string(raw), "\n")
152 out := lines[:0]
153 for _, line := range lines {
154 t := strings.TrimSpace(line)
155 if t != "" && !strings.HasPrefix(t, "#") {
156 if k, _, ok := strings.Cut(t, "="); ok && strings.TrimSpace(k) == InstanceKeyVar {
157 removed = true
158 continue
159 }
160 }
161 out = append(out, line)
162 }
163 return []byte(strings.Join(out, "\n")), removed
164}
165