1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
package devinstance
import (
"cmp"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/johnandersen777/socialweb-computer-kcp/internal/bootstrap/table"
"github.com/johnandersen777/socialweb-computer-kcp/internal/hostsvc"
)
const RoleRequester = "requester"
const basenameChars = 16
var namePattern = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*$`)
var ErrRoot = errors.New("devinstance: dev refuses to run as root (uid 0): dev mode needs no root; production uses install")
type Env interface {
LookupEnv(key string) (string, bool)
}
func sanitize(s string) string {
s = strings.ToLower(s)
var b strings.Builder
for _, r := range s {
switch {
case r >= 'a' && r <= 'z', r >= '0' && r <= '9':
b.WriteRune(r)
default:
b.WriteByte('-')
}
}
out := strings.Trim(b.String(), "-")
if len(out) > basenameChars {
out = strings.Trim(out[:basenameChars], "-")
}
out = cmp.Or(out, "dev")
return out
}
func hash8(s string) string {
sum := sha256.Sum256([]byte(s))
return hex.EncodeToString(sum[:])[:8]
}
func WorktreeRoot(dir string) string {
d := dir
for {
if _, err := os.Stat(filepath.Join(d, ".git")); err == nil {
return d
}
parent := filepath.Dir(d)
if parent == d {
return dir
}
d = parent
}
}
func within(dir, root string) bool {
rel, err := filepath.Rel(root, dir)
return err == nil && rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator))
}
func readableName(root string, env Env) string {
task, _ := env.LookupEnv("TASK_NAME")
taskRoot, _ := env.LookupEnv("TASK_ROOT")
if task != "" && taskRoot != "" {
if real, err := filepath.EvalSymlinks(taskRoot); err == nil && within(root, real) {
return sanitize(task)
}
}
return sanitize(filepath.Base(root))
}
func ID(name, cwd string, env Env) (string, error) {
if name != "" {
if len(name) > 40 || !namePattern.MatchString(name) {
return "", fmt.Errorf("devinstance: instance %q: want lower-case letters, digits and dashes, at most 40", name)
}
return name, nil
}
real, err := filepath.EvalSymlinks(cwd)
if err != nil {
return "", err
}
root := WorktreeRoot(real)
return readableName(root, env) + "-" + hash8(root), nil
}
func Short(id string) string { return hash8("swc-instance:" + id) }
func StateRoot(option string, env Env) (string, error) {
if option != "" {
return filepath.Abs(option)
}
if v, ok := env.LookupEnv("TASK_STATE_DIR"); ok && v != "" {
return filepath.Join(v, "socialweb-computer"), nil
}
if v, ok := env.LookupEnv("XDG_STATE_HOME"); ok && v != "" {
return filepath.Join(v, "socialweb-computer"), nil
}
home, ok := env.LookupEnv("HOME")
if !ok || home == "" {
return "", errors.New("devinstance: neither state-dir, TASK_STATE_DIR, XDG_STATE_HOME nor HOME is set")
}
return filepath.Join(home, ".local", "state", "socialweb-computer"), nil
}
func RuntimeRoot(stateRoot string, env Env) (string, error) {
if v, ok := env.LookupEnv("XDG_RUNTIME_DIR"); ok && v != "" {
return filepath.Join(v, "swc"), nil
}
if stateRoot != "" {
return filepath.Join(stateRoot, "run"), nil
}
return "", errors.New("devinstance: neither XDG_RUNTIME_DIR (a login session or linger provides it) nor a state root is set")
}
type Layout struct {
ID string `json:"id"`
Short string `json:"short"`
StateDir string `json:"stateDir"`
RuntimeDir string `json:"runtimeDir"`
Socket string `json:"socket"`
SupState string `json:"supState"`
Record string `json:"record"`
CoreRecord string `json:"coreRecord"`
Lock string `json:"lock"`
CoreLock string `json:"coreLock"`
Logs string `json:"logs"`
KineDir string `json:"kineDir"`
KcpRoot string `json:"kcpRoot"`
Prefix string `json:"prefix,omitempty"`
}
func NewLayout(id, stateRoot, runtimeRoot string) (Layout, error) {
short := Short(id)
state := filepath.Join(DevRoot(stateRoot), id)
rt := filepath.Join(runtimeRoot, short)
l := Layout{
ID: id, Short: short, StateDir: state, RuntimeDir: rt,
Socket: filepath.Join(rt, "sup.sock"),
SupState: filepath.Join(state, "sup"),
Record: filepath.Join(state, "instance.json"),
CoreRecord: filepath.Join(state, "core.json"),
Lock: filepath.Join(state, "instance.lock"),
CoreLock: filepath.Join(state, "core.lock"),
Logs: filepath.Join(state, "logs"),
KineDir: filepath.Join(state, "kcp", "kine-a"),
KcpRoot: filepath.Join(state, "kcp", "slot-a"),
}
if n := len(l.Socket); n > MaxSocketPath {
return Layout{}, fmt.Errorf("devinstance: socket path %s is %d bytes, over the %d-byte sun_path limit", l.Socket, n, MaxSocketPath)
}
return l, nil
}
func (l Layout) UnitPrefix() string {
if l.Prefix != "" {
return l.Prefix
}
return hostsvc.NamePrefix
}
func (l Layout) Names() table.Names { return table.Names{Prefix: l.UnitPrefix(), Key: l.Short} }
func (l Layout) KineUnit() string { return l.Names().Kine(table.SlotA) }
func (l Layout) KcpUnit() string { return l.Names().Slot(table.SlotA) }
func (l Layout) CoreUnit() string { return l.Names().DevCore() }
func (l Layout) KcpSlice() string { return l.Names().Slice() }
func (l Layout) CoreSlice() string { return l.Names().CoreSlice() }
func (l Layout) AdminKubeconfig() string { return filepath.Join(l.KcpRoot, "admin.kubeconfig") }
type Executable struct {
Path string `json:"path"`
SHA256 string `json:"sha256"`
Version string `json:"version"`
}
type Unit struct {
Role string `json:"role"`
Unit string `json:"unit"`
Identity hostsvc.Identity `json:"identity"`
Port int `json:"port"`
Endpoint string `json:"endpoint"`
Adopted bool `json:"adopted"`
PlacedAt time.Time `json:"placedAt"`
PortAt time.Time `json:"portAt"`
ReadyAt time.Time `json:"readyAt,omitzero"`
RSSKiB int64 `json:"rssKiB,omitempty"`
StartedTo string `json:"startToReady,omitempty"`
}
type StandIn struct {
Role string `json:"role"`
Exec []string `json:"exec"`
Env []string `json:"env,omitempty"`
Listens bool `json:"listens,omitempty"`
}
type Devsup struct {
Instance string `json:"instance"`
PID int `json:"pid"`
StartTicks uint64 `json:"startTicks"`
BootID string `json:"bootID"`
Adopted bool `json:"adopted"`
}
type Record struct {
Layout Layout `json:"layout"`
Worktree string `json:"worktree,omitempty"`
Profile string `json:"profile"`
Backend hostsvc.Backend `json:"backend"`
BaseWorkspace string `json:"baseWorkspace"`
SharedKcp string `json:"sharedKcp,omitempty"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
Devsup Devsup `json:"devsup"`
StandIns []StandIn `json:"standIns,omitempty"`
Units []Unit `json:"units"`
AdminKubeconfig string `json:"adminKubeconfig"`
KcpURL string `json:"kcpURL"`
KineURL string `json:"kineURL"`
Executables map[string]Executable `json:"executables"`
SourceRoot string `json:"sourceRoot,omitempty"`
OpenBao *OpenBao `json:"openbao,omitempty"`
Ready bool `json:"ready"`
Host bool `json:"host,omitempty"`
Parent *Parent `json:"parent,omitempty"`
VM *VMRuntime `json:"vm,omitempty"`
Gateway *GatewayChild `json:"gateway,omitempty"`
Role string `json:"role,omitempty"`
}
type GatewayChild struct {
SpecFile string `json:"specFile"`
BootstrapCluster string `json:"bootstrapCluster,omitempty"`
BootstrapAccountDID string `json:"bootstrapAccountDid,omitempty"`
}
type VMRuntime struct {
Handle string `json:"handle"`
Backend hostsvc.Backend `json:"backend"`
ImageSource string `json:"imageSource,omitempty"`
StopGrace string `json:"stopGrace,omitempty"`
RuntimeDir string `json:"runtimeDir"`
Executables map[string]string `json:"executables"`
}
type OpenBao struct {
Address string `json:"address"`
TokenFile string `json:"tokenFile"`
}
func (r *Record) Unit(role string) *Unit {
for i := range r.Units {
if r.Units[i].Role == role {
return &r.Units[i]
}
}
return nil
}
func (r *Record) SetUnit(u Unit) {
if cur := r.Unit(u.Role); cur != nil {
*cur = u
return
}
r.Units = append(r.Units, u)
}
type Adoption struct {
Role string `json:"role"`
Unit string `json:"unit"`
InvocationID string `json:"invocationID"`
PID int `json:"pid"`
Adopted bool `json:"adopted"`
}
type Core struct {
Generation int `json:"generation"`
PID int `json:"pid"`
InvocationID string `json:"invocationID"`
StartedAt time.Time `json:"startedAt"`
AdoptedAt time.Time `json:"adoptedAt"`
Adoptions []Adoption `json:"adoptions"`
LastCheck time.Time `json:"lastCheck"`
Verdicts []string `json:"verdicts"`
BusDenied uint64 `json:"busDenied"`
BusDeniedBy map[string]uint64 `json:"busDeniedBy,omitempty"`
}
func RefuseRoot(euid int) error {
if euid == 0 {
return ErrRoot
}
return nil
}