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
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
package main
import (
"cmp"
"context"
"errors"
"fmt"
"io"
"log/slog"
"maps"
"os"
"os/exec"
"path/filepath"
"slices"
"strings"
"sync"
"time"
"github.com/bluesky-social/indigo/atproto/syntax"
"github.com/johnandersen777/socialweb-computer-kcp/api/v1alpha1"
"github.com/johnandersen777/socialweb-computer-kcp/internal/app/compose"
"github.com/johnandersen777/socialweb-computer-kcp/internal/app/marketrequester"
"github.com/johnandersen777/socialweb-computer-kcp/internal/app/sshgateway"
"github.com/johnandersen777/socialweb-computer-kcp/internal/clicfg"
"github.com/johnandersen777/socialweb-computer-kcp/internal/devinstance"
"github.com/johnandersen777/socialweb-computer-kcp/internal/hostsvc"
"github.com/johnandersen777/socialweb-computer-kcp/internal/kit/fsx"
"github.com/johnandersen777/socialweb-computer-kcp/internal/stack/request"
)
const (
requesterVerb = "run requester"
requesterReadyRead = 10 * time.Second
requesterRootApply = 3 * time.Minute
)
var requesterOptions = []string{backendOpt, "network", "name", "identity", "state-dir", "ready-file", "fg", "ready-timeout", "network-timeout",
"kcp-path", "kine-path", "bao-path", "dumbpipe-path", "asset-cache", "log-level", "unit-prefix", "with-gateway", "node-download-base"}
var requesterPasswordOptions = []string{"name", "state-dir", "network-timeout"}
var requesterSigninOptions = []string{"name", "state-dir", "json"}
var requesterHelperOptions = []string{"name", "state-dir", "ready-file", "json"}
var requesterSSHOptions = []string{"name", "state-dir", "ready-file", "contract", "transport", "tag", "exec", "contract-timeout", "network-timeout",
"dumbpipe-path", "asset-cache", "log-level"}
func parseRequesterSpec(args []string, verb string, verbOptions []string, network bool, other func(compose.AppArg) (bool, error)) (v1alpha1.MarketRequesterSpec, error) {
root := v1alpha1.MarketRequesterSpec{Profile: marketrequester.RequesterProfileDefault,
Identity: v1alpha1.ApplicationIdentity{Mode: v1alpha1.MarketIdentityModeLocalRepo}}
parsed, err := compose.ParseAppArgs(marketrequester.RequesterApp, args)
if err != nil {
return root, err
}
var nv v1alpha1.MarketNetworkValue
var options []v1alpha1.ApplicationOption
nvSet, expectHandle := false, false
for _, p := range parsed {
switch p.Name {
case "identity", "pds-state-dir", "expected-pds-url":
return root, fmt.Errorf("--%s is reserved; pass global flags before --", p.Name)
}
networkIdentity := network && slices.Contains(compose.NetworkRequesterIdentityOptions, p.Name)
if networkIdentity {
if p.Name == compose.OptionIdentityMode && v1alpha1.MarketIdentityMode(p.Value) != v1alpha1.MarketIdentityModePdsAccount {
return root, fmt.Errorf("--%s %s: a market network requester takes the identity mode %s only", p.Name, p.Value, v1alpha1.MarketIdentityModePdsAccount)
}
options = append(options, v1alpha1.ApplicationOption{Name: p.Name, Value: p.Value})
}
perApp := networkIdentity || p.To == "spec.secretsFallback" || p.To == "spec.comms.mode" || p.To == "spec.comms.eventSourceRefs" || strings.HasPrefix(p.To, "spec.requesterIngress")
if network && !perApp && (strings.HasPrefix(p.To, compose.MapToSpec) || strings.HasPrefix(p.To, compose.MapToNetwork)) {
return root, fmt.Errorf("--%s maps to %s, which the market network sets for its child applications", p.Name, p.To)
}
switch {
case p.To == compose.MapToOptions && !p.Mapped && slices.Contains(verbOptions, p.Name):
return root, fmt.Errorf("--%s is a %s option; pass it before --", p.Name, verb)
case p.To == compose.MapToOptions && !p.Mapped:
return root, fmt.Errorf("--%s is no %s option (%s); %s does not pass unknown options on", p.Name, marketrequester.RequesterApp, compose.OptionMapPath, verb)
case p.To == compose.MapToOptions:
options = append(options, v1alpha1.ApplicationOption{Name: p.Name, Value: p.Value})
case p.To == "spec.identity.mode":
switch p.Name {
case "atproto-oauth":
root.Identity.Mode = v1alpha1.MarketIdentityModeOAuthBrowser
case "atproto-oauth-qr":
root.Identity.Mode = v1alpha1.MarketIdentityModeOAuthQr
default:
root.Identity.Mode = v1alpha1.MarketIdentityMode(p.Value)
}
case p.To == "spec.identity.handle":
root.Identity.Handle = p.Value
expectHandle = expectHandle || p.Name == "expect-handle"
case p.To == "spec.identity.pdsUrl":
root.Identity.PdsURL = p.Value
case p.To == "spec.identity.association":
if p.Value != "false" {
root.Identity.Association = v1alpha1.AssociationSkip
}
case p.To == "spec.bidderDids":
root.BidderDids = append(root.BidderDids, splitList(p.Value)...)
case strings.HasPrefix(p.To, "spec.requesterIngress"):
var applied bool
if root.RequesterIngress, applied, err = applyIngressOption(root.RequesterIngress, p.Name, p.Value); err != nil {
return root, err
}
if !applied {
return root, fmt.Errorf("--%s maps to %s, which %s does not take", p.Name, p.To, verb)
}
case strings.HasPrefix(p.To, "spec.ingress"):
var applied bool
if root.Ingress, applied, err = applyIngressOption(root.Ingress, p.Name, p.Value); err != nil {
return root, err
}
if !applied {
return root, fmt.Errorf("--%s maps to %s, which %s does not take", p.Name, p.To, verb)
}
case p.To == "spec.secretsFallback":
switch fallback := v1alpha1.SecretsFallback(strings.TrimSpace(p.Value)); fallback {
case v1alpha1.SecretsFallbackNone, v1alpha1.SecretsFallbackSealed:
root.SecretsFallback = fallback
default:
return root, fmt.Errorf("--secrets-fallback %q: want None or Sealed", p.Value)
}
case strings.HasPrefix(p.To, "spec.comms"):
var applied bool
if root.Comms, applied, err = applyCommsOption(root.Comms, p.Name, p.Value); err != nil {
return root, err
}
if !applied {
return root, fmt.Errorf("--%s maps to %s, which %s does not take", p.Name, p.To, verb)
}
case strings.HasPrefix(p.To, compose.MapToNetwork):
nvSet = true
switch p.To {
case "network.plcDirectoryUrl":
nv.PLCDirectoryURL = p.Value
case "network.ingressProxyHost":
nv.IngressProxyHost = p.Value
case "network.qrOrigin":
nv.QrOrigin = p.Value
case "network.hosts":
for _, h := range splitList(p.Value) {
name, addr, ok := strings.Cut(h, "=")
if !ok {
return root, fmt.Errorf("--network-hosts %q: want name=address,...", p.Value)
}
nv.Hosts = append(nv.Hosts, v1alpha1.NetworkHost{Name: name, Address: addr})
}
}
default:
handled := false
if other != nil {
if handled, err = other(p); err != nil {
return root, err
}
}
if !handled {
return root, fmt.Errorf("--%s maps to %s, which %s does not take", p.Name, p.To, verb)
}
}
}
if expectHandle {
switch root.Identity.Mode {
case v1alpha1.MarketIdentityModeOAuthQr, v1alpha1.MarketIdentityModeOAuthBrowser, v1alpha1.MarketIdentityModeOAuthDelegated:
default:
return root, fmt.Errorf("--expect-handle pins the account of an oauth sign-in; the identity mode %s signs in no existing account, add --atproto-oauth-qr", root.Identity.Mode)
}
if _, err := syntax.ParseHandle(strings.TrimSpace(root.Identity.Handle)); err != nil {
return root, fmt.Errorf("--expect-handle %q: %w", root.Identity.Handle, err)
}
}
if network && root.Identity.Mode == v1alpha1.MarketIdentityModePdsAccount && strings.TrimSpace(root.Identity.Handle) == "" {
return root, fmt.Errorf("--%s %s needs --atproto-handle naming the account", compose.OptionIdentityMode, v1alpha1.MarketIdentityModePdsAccount)
}
if nvSet || !network {
nv.PLCDirectoryURL = cmp.Or(nv.PLCDirectoryURL, "https://plc.directory")
nv.IngressProxyHost = cmp.Or(nv.IngressProxyHost, "xrpc.fedproxy.com")
root.Network = &nv
}
if !network && root.Ingress == nil {
root.Ingress = compose.DefaultIngressConfig()
if nv.IngressProxyHost != "" {
root.Ingress.Xrpc.IngressProxyHost = nv.IngressProxyHost
}
}
root.Options = options
return root, nil
}
type requesterSummary struct {
request.Ready
Mode string `json:"mode"`
Network string `json:"network,omitempty"`
Instance string `json:"instance"`
ReadyFile string `json:"readyFile"`
Restarted bool `json:"restarted"`
Seconds float64 `json:"seconds"`
}
type requesterRun struct {
*requestRun
dir string
readyFiles []string
startedAt time.Time
sshKey string
mu sync.Mutex
last request.Ready
gateway *request.ReadyGateway
gatewayOn bool
gatewayExe map[string]devinstance.Executable
instance *devinstance.Record
}
func requesterHome(a *app, vals clicfg.Values, name string) (string, string, error) {
root, err := devinstance.StateRoot(vals.String("state-dir"), a.env)
if err != nil {
return "", "", err
}
if root, err = filepath.Abs(root); err != nil {
return "", "", err
}
if err := request.CheckName(name); err != nil {
return "", "", err
}
return root, request.RequesterDir(root, name), nil
}
func requesterReadyPath(vals clicfg.Values, dir string) (string, error) {
path := strings.TrimSpace(vals.String("ready-file"))
if path == "" {
return filepath.Join(dir, request.ReadyFile), nil
}
return filepath.Abs(path)
}
func runRequester(ctx context.Context, a *app, vals clicfg.Values, rest []string) int {
started := time.Now()
pos, pass := splitWords(rest)
if len(pos) > 0 {
return a.fail(exitRefused, "run requester: unexpected %q; MarketRequester options go after --", strings.Join(pos, " "))
}
backend := hostsvc.BackendProc
if b := vals.String(backendOpt); b != "" {
parsed, err := hostsvc.ParseBackend(b)
if err != nil {
return a.fail(exitRefused, "run requester: %v", err)
}
backend = parsed
}
if _, err := exec.LookPath("ssh-keygen"); err != nil {
return a.fail(exitRefused, "run requester: ssh-keygen is not on PATH; install the OpenSSH client (apt install openssh-client, dnf install openssh-clients)")
}
name := cmp.Or(strings.TrimSpace(vals.String("name")), "requester")
stateRoot, dir, err := requesterHome(a, vals, name)
if err != nil {
return a.fail(exitRefused, "run requester: %v", err)
}
network := strings.TrimSpace(vals.String("network"))
if network != "" && backend == hostsvc.BackendSystemdUser {
return a.fail(exitRefused, "run requester: --network places the requester under the market network's devsup; pass --host-backend=proc")
}
idName := strings.TrimSpace(vals.String("identity"))
parsed, err := parseRequesterSpec(pass, requesterVerb, requesterOptions, network != "", nil)
if err != nil {
return a.fail(exitRefused, "run requester: %v", err)
}
readyFile, err := requesterReadyPath(vals, dir)
if err != nil {
return a.fail(exitRefused, "run requester: --ready-file: %v", err)
}
networkTimeout, err := time.ParseDuration(vals.String("network-timeout"))
if err != nil {
return a.fail(exitRefused, "run requester: network-timeout: %v", err)
}
if err := request.EnsureRequesterDir(dir); err != nil {
return a.fail(exitFailed, "run requester: %v", err)
}
owner, owned, err := request.TakeOwnerDir(dir)
if err != nil {
return a.fail(exitFailed, "run requester: %v", err)
}
if !owned {
return a.fail(exitRefused, "run requester: another process runs the requester %s (%s); run requester env|ssh %s reach it", name, dir, name)
}
defer owner.Close()
rec := request.RequesterRecord{Schema: request.RequesterSchema, Name: name, Network: network, Identity: idName, Spec: parsed,
WithGateway: vals.Bool("with-gateway"), CreatedAt: time.Now().UTC()}
saved, restarted, err := request.ReadRequesterRecord(dir)
if err != nil {
return a.fail(exitFailed, "run requester: %v", err)
}
if restarted {
switch {
case len(pass) == 0 && saved.Network == network && saved.Identity == idName:
rec = saved
case !saved.SameShape(rec):
return a.fail(exitRefused, "run requester: the requester %s was created with other options (network %q, identity %q, its own -- options in %s); start it with the same options, or with none after --",
name, saved.Network, saved.Identity, filepath.Join(dir, request.RequesterFile))
default:
rec.CreatedAt = saved.CreatedAt
}
if want := saved.WithGateway || vals.Bool("with-gateway"); want != saved.WithGateway {
rec.WithGateway = want
if err := request.WriteRequesterRecord(dir, rec); err != nil {
return a.fail(exitFailed, "run requester: %v", err)
}
} else {
rec.WithGateway = want
}
} else if err := request.WriteRequesterRecord(dir, rec); err != nil {
return a.fail(exitFailed, "run requester: %v", err)
}
mode := request.ModeStandalone
if network != "" {
mode = request.ModeNetwork
}
rr := &requestRun{a: a, vals: vals, stateRoot: stateRoot, homeDir: dir, verb: requesterVerb, root: rec.Spec, appOptions: rec.Spec.Options,
networkTimeout: networkTimeout, resuming: true, transport: transportIroh}
rr.inv = request.Invocation{Name: name, Mode: mode, Network: network, StateRoot: stateRoot, CreatedAt: rec.CreatedAt}
rr.summary = request.Summary{Name: name, Mode: mode, Network: network, CreatedAt: rec.CreatedAt}
r := &requesterRun{requestRun: rr, dir: dir, readyFiles: []string{filepath.Join(dir, request.ReadyFile)}, startedAt: time.Now().UTC()}
if readyFile != r.readyFiles[0] {
r.readyFiles = append(r.readyFiles, readyFile)
}
if rec.WithGateway {
if r.gatewayExe, err = r.gatewayPreflight(vals); err != nil {
return a.fail(exitRefused, "run requester: %v", err)
}
if err := r.markGateway(ctx); err != nil {
return a.fail(exitRefused, "run requester: %v", err)
}
r.gatewayOn = true
}
if err := r.openIdentity(name); err != nil {
return a.fail(exitRefused, "run requester: %v", err)
}
defer func() {
if r.identity != nil {
r.identity.Close()
}
}()
pub, key, err := request.EnsureKey(ctx, filepath.Join(dir, request.SSHDir))
if err != nil {
return a.fail(exitFailed, "run requester: %v", err)
}
r.sshKey = key + ".pub"
if err := r.linkIdentityExposure(); err != nil {
return a.fail(exitFailed, "run requester: %v", err)
}
for _, path := range r.readyFiles {
if err := request.RemoveReady(path); err != nil {
return a.fail(exitFailed, "run requester: %v", err)
}
}
fresh := true
if mode == request.ModeNetwork {
_, netRec, err := r.networkLayout()
if err != nil {
return a.fail(exitRefused, "run requester: %v", err)
}
_, found, err := request.NetworkApp(ctx, netRec.AdminKubeconfig, name)
if err != nil {
return a.fail(exitFailed, "run requester: %v", err)
}
fresh = !found
}
if backend == hostsvc.BackendSystemdUser {
r.log("run requester: the installed requester; its unit stops the core and keeps its kcp units and state", "dir", dir, "restart", restarted)
return r.installedCore(ctx, stateRoot, func(ctx context.Context, rec devinstance.Record, notifyReady func() error) error {
r.inv.Instance, r.summary.Instance, r.summary.KcpURL = rec.Layout.ID, rec.Layout.ID, rec.KcpURL
r.instance = &rec
if err := r.attach(ctx, rec.AdminKubeconfig); err != nil {
return err
}
return r.serve(ctx, started, readyFile, pub, restarted, notifyReady)
})
}
r.log("run requester: a long-lived requester; Ctrl-C stops its kcp and keeps its state", "dir", dir, "mode", mode, "network", network, "restart", restarted)
if err := r.start(ctx, fresh); err != nil {
r.stop(context.WithoutCancel(ctx), false)
if ctx.Err() != nil {
return a.fail(exitFailed, "run requester: stopped before the requester was Ready; its kcp is stopped and its state is kept in %s", dir)
}
return a.fail(exitFailed, "run requester: %v", err)
}
err = r.serve(ctx, started, readyFile, pub, restarted, func() error { return nil })
dctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), fgDownTimeout)
defer cancel()
r.stop(dctx, false)
if err != nil {
return a.fail(exitFailed, "run requester: %v", err)
}
return exitOK
}
func (r *requesterRun) attach(ctx context.Context, kubeconfig string) error {
k, err := request.OpenKcp(kubeconfig, "")
if err != nil {
return err
}
r.kubeconfig, r.workspace = kubeconfig, ""
r.kcp = k.ForContract(r.target)
if err := applyRequesterRoot(ctx, k, r.root, r.a.log); err != nil {
return err
}
r.summary.KcpRunning = true
r.log("the requester's kcp runs", "kcp", r.summary.KcpURL, "instance", r.inv.Instance)
if err := r.signIn(ctx, true); err != nil {
return fmt.Errorf("signing in: %w", err)
}
if err := r.kcp.WaitReady(ctx, r.networkTimeout, func(st v1alpha1.ApplicationStatus) {
r.log("requester", "phase", string(st.Phase), "message", st.Message, "components", len(st.Components))
}); err != nil {
return err
}
return request.WriteInvocationIn(r.home(), r.inv)
}
func applyRequesterRoot(ctx context.Context, k *request.Kcp, spec v1alpha1.MarketRequesterSpec, log *slog.Logger) error {
started := time.Now()
delay := time.Second
for attempt := 1; ; attempt++ {
actx, cancel := context.WithTimeout(ctx, requesterRootApply)
err := k.ApplyRoot(actx, spec)
cancel()
if err == nil {
return nil
}
if time.Since(started) >= requesterRootApply {
return fmt.Errorf("applying the MarketRequester root object after %d attempts: %w", attempt, err)
}
log.Warn("applying the MarketRequester root object; retrying", "attempt", attempt, "err", err.Error())
t := time.NewTimer(delay)
select {
case <-ctx.Done():
t.Stop()
return ctx.Err()
case <-t.C:
}
delay = min(2*delay, 15*time.Second)
}
}
func (r *requesterRun) installedCore(ctx context.Context, stateRoot string, serve func(context.Context, devinstance.Record, func() error) error) int {
a, vals := r.a, r.vals
exes := map[string]devinstance.Executable{}
for _, x := range []struct{ name, option string }{{request.ExeKcp, "kcp-path"}, {request.ExeKine, "kine-path"}, {"bao", "bao-path"},
{sshgateway.DumbpipeExecutable, "dumbpipe-path"}} {
e, err := a.optionalExe(ctx, vals, x.name, x.option)
if err != nil {
return a.fail(exitRefused, "run requester: %v", err)
}
if e.Path != "" {
exes[x.name] = e
}
}
plugins, err := a.openBaoPluginExes(vals)
if err != nil {
return a.fail(exitRefused, "run requester: %v", err)
}
maps.Copy(exes, plugins)
exe, err := selfExe()
if err != nil {
return a.fail(exitFailed, "run requester: %v", err)
}
exes["socialweb-computer"] = devinstance.Executable{Path: exe}
readyTimeout, err := time.ParseDuration(vals.String("ready-timeout"))
if err != nil || readyTimeout < 0 {
return a.fail(exitRefused, "run requester: ready-timeout %q: want a positive duration", vals.String("ready-timeout"))
}
prefix := vals.String("unit-prefix")
m, err := hostsvc.Open(ctx, hostsvc.Config{Backend: hostsvc.BackendSystemdUser, UnitPrefix: prefix})
if err != nil {
return a.fail(exitFailed, "run requester: the systemd-user backend: %v", err)
}
defer m.Close()
err = request.RunCore(ctx, request.CoreConfig{StateDir: stateRoot, Prefix: prefix, Name: r.inv.Name, Executables: exes,
ReadyTimeout: readyTimeout, Log: a.log, Serve: serve}, m)
if err != nil && !errors.Is(err, context.Canceled) {
return a.fail(exitFailed, "run requester: %v", err)
}
return exitOK
}
func (r *requesterRun) serve(ctx context.Context, started time.Time, readyFile, pub string, restarted bool, notifyReady func() error) error {
dir := r.dir
if err := r.checkIdentity(); err != nil {
return err
}
if err := request.WriteWorkspaceKubeconfig(filepath.Join(dir, request.KubeconfigFile), r.kubeconfig, r.workspace); err != nil {
return fmt.Errorf("the workspace kubeconfig: %w", err)
}
st, err := r.kcp.Root(ctx)
if err != nil {
return err
}
ready := r.ready(ctx, st)
gctx, stopGateway := context.WithCancel(ctx)
defer stopGateway()
var gatewayDone <-chan error
if r.gatewayOn {
done, gw, err := r.startGateway(gctx, r.gatewayExe, ready.Did)
if err != nil {
return fmt.Errorf("--with-gateway: %w", err)
}
gatewayDone = done
r.mu.Lock()
r.gateway = gw
r.mu.Unlock()
ready = r.ready(ctx, st)
r.log("the requester's gateway is Ready; the ready file names it", "workspace", gw.Workspace, "clusters", gw.Clusters)
}
if err := r.writeReady(ready); err != nil {
return fmt.Errorf("the ready file: %w", err)
}
r.log("the requester is Ready; the ready file is written", "readyFile", readyFile, "did", ready.Did, "kcp", ready.KcpURL, "workspace", ready.Workspace,
"sshPublicKey", pub, "gateway", ready.Gateway != nil)
r.a.printJSON(requesterSummary{Ready: ready, Mode: r.inv.Mode, Network: r.inv.Network, Instance: r.inv.Instance, ReadyFile: readyFile, Restarted: restarted,
Seconds: time.Since(started).Seconds()})
if err := notifyReady(); err != nil {
return err
}
follow, stopFollow := context.WithCancel(ctx)
var wg sync.WaitGroup
wg.Go(func() {
if err := r.kcp.FollowRoot(follow, func(o *v1alpha1.MarketRequester) {
if err := r.writeReady(r.ready(follow, o.Status)); err != nil {
r.a.log.Warn("rewriting the ready file", "err", err.Error())
}
}); err != nil && follow.Err() == nil {
r.a.log.Warn("following the MarketRequester for the ready file", "err", err.Error())
}
})
var ended error
select {
case <-ctx.Done():
case err := <-gatewayDone:
if ctx.Err() == nil {
ended = fmt.Errorf("the requester's gateway ended: %v", err)
}
}
stopGateway()
stopFollow()
wg.Wait()
for _, path := range r.readyFiles {
if err := request.RemoveReady(path); err != nil {
r.a.log.Warn("removing the ready file", "path", path, "err", err.Error())
}
}
return ended
}
func runRequesterPassword(ctx context.Context, a *app, vals clicfg.Values, rest []string) int {
pos, pass := splitWords(rest)
if len(pos) != 1 || len(pass) > 0 {
return a.fail(exitRefused, "run requester password <name>: one requester name; the app password comes on stdin")
}
_, dir, err := requesterHome(a, vals, pos[0])
if err != nil {
return a.fail(exitRefused, "run requester password: %v", err)
}
timeout, err := time.ParseDuration(vals.String("network-timeout"))
if err != nil {
return a.fail(exitRefused, "run requester password: network-timeout: %v", err)
}
var rec devinstance.Record
if err := fsx.ReadJSON(filepath.Join(request.InstanceStateDirIn(dir, pos[0]), "instance.json"), &rec); err != nil || rec.AdminKubeconfig == "" {
return a.fail(exitRefused, "run requester password: the requester %s has no running kcp to hold the password (start it first): %v", pos[0], err)
}
b, err := io.ReadAll(io.LimitReader(os.Stdin, 4096))
if err != nil {
return a.fail(exitFailed, "run requester password: reading stdin: %v", err)
}
password := strings.TrimSpace(string(b))
if password == "" {
return a.fail(exitRefused, "run requester password: stdin held no password")
}
k, err := request.OpenKcp(rec.AdminKubeconfig, rec.BaseWorkspace)
if err != nil {
return a.fail(exitFailed, "run requester password: %v", err)
}
ref, err := k.PutPdsAccountPassword(ctx, password, timeout)
if err != nil {
return a.fail(exitFailed, "run requester password: %v", err)
}
a.printJSON(map[string]string{"name": pos[0], "mount": ref.Mount, "path": ref.Path})
return exitOK
}
type requesterSignin struct {
Name string `json:"name"`
Phase v1alpha1.MarketIdentityPhase `json:"phase"`
Did string `json:"did,omitempty"`
Handle string `json:"handle,omitempty"`
Message string `json:"message,omitempty"`
PendingAction *v1alpha1.MarketIdentityPendingAction `json:"pendingAction,omitempty"`
}
func runRequesterSignin(ctx context.Context, a *app, vals clicfg.Values, rest []string) int {
pos, pass := splitWords(rest)
if len(pos) != 1 || len(pass) > 0 {
return a.fail(exitRefused, "run requester signin <name>: one requester name")
}
_, dir, err := requesterHome(a, vals, pos[0])
if err != nil {
return a.fail(exitRefused, "run requester signin: %v", err)
}
var rec devinstance.Record
if err := fsx.ReadJSON(filepath.Join(request.InstanceStateDirIn(dir, pos[0]), "instance.json"), &rec); err != nil || rec.AdminKubeconfig == "" {
return a.fail(exitRefused, "run requester signin: the requester %s has no kcp of its own under %s (start it first; a --network requester renders its sign-in in its own terminal): %v", pos[0], dir, err)
}
k, err := request.OpenKcp(rec.AdminKubeconfig, rec.BaseWorkspace)
if err != nil {
return a.fail(exitFailed, "run requester signin: %v", err)
}
ids, err := k.Identities()
if err != nil {
return a.fail(exitFailed, "run requester signin: %v", err)
}
gctx, cancel := context.WithTimeout(ctx, requesterReadyRead)
defer cancel()
obj, err := ids.GetMarketIdentity(gctx, marketrequester.RequesterIdentity)
if err != nil {
return a.fail(exitFailed, "run requester signin: the requester %s's kcp does not answer or holds no identity yet (is its unit running?): %v", pos[0], err)
}
out := requesterSignin{Name: pos[0], Phase: obj.Status.Phase, Did: obj.Status.Did, Handle: obj.Status.Handle, Message: obj.Status.Message,
PendingAction: obj.Status.PendingAction}
if vals.Bool("json") {
a.printJSON(out)
return exitOK
}
switch {
case out.PendingAction != nil && out.PendingAction.URL != "":
fmt.Fprintf(a.stderr, "identity %s is %s: %s; open the URL below (scripts/oauth-signin.ts --url) before %s\n", marketrequester.RequesterIdentity,
out.Phase, cmp.Or(out.Message, string(out.PendingAction.Kind)), cmp.Or(out.PendingAction.ExpiresAt, "it is replaced"))
fmt.Fprintln(a.stdout, out.PendingAction.URL)
return exitOK
case out.Phase == v1alpha1.MarketIdentityPhaseReady:
fmt.Fprintf(a.stderr, "identity %s is Ready as %s (%s); no sign-in is pending\n", marketrequester.RequesterIdentity, cmp.Or(out.Handle, "no handle"), out.Did)
return exitOK
case out.PendingAction != nil:
return a.fail(exitFailed, "run requester signin: identity %s is %s and waits on %s, not on a sign-in URL: %s", marketrequester.RequesterIdentity, out.Phase,
out.PendingAction.Kind, cmp.Or(out.PendingAction.Message, out.Message))
}
return a.fail(exitFailed, "run requester signin: identity %s is %s with no sign-in URL yet: %s", marketrequester.RequesterIdentity, cmp.Or(string(out.Phase), "unset"), out.Message)
}
func (r *requesterRun) ready(ctx context.Context, st v1alpha1.ApplicationStatus) request.Ready {
var id *v1alpha1.MarketIdentity
if ids, err := r.kcp.Identities(); err == nil {
ictx, cancel := context.WithTimeout(ctx, requesterReadyRead)
if obj, err := ids.GetMarketIdentity(ictx, marketrequester.RequesterIdentity); err == nil {
id = obj
}
cancel()
}
did, handle, mode, pdsURL, ingress := request.ReadyOf(st, id)
r.mu.Lock()
gw := r.gateway
r.mu.Unlock()
return request.Ready{Gateway: gw, Schema: request.ReadySchema, Name: r.inv.Name, Pid: os.Getpid(), StartedAt: r.startedAt, KcpURL: r.summary.KcpURL,
Kubeconfig: filepath.Join(r.dir, request.KubeconfigFile), Workspace: cmp.Or(r.workspace, compose.DefaultWorkspace), Did: did, Handle: handle,
IdentityMode: mode, PdsURL: pdsURL, Ingress: ingress, SSHPublicKey: r.sshKey}
}
func (r *requesterRun) writeReady(ready request.Ready) error {
r.mu.Lock()
defer r.mu.Unlock()
if ready.Did == "" {
ready.Did = r.last.Did
}
if r.last.Schema != 0 && r.last.Same(ready) {
return nil
}
for _, path := range r.readyFiles {
if err := request.WriteReady(path, ready); err != nil {
return err
}
}
if r.last.Schema != 0 {
r.log("the ready file is rewritten", "did", ready.Did, "kcp", ready.KcpURL, "pdsIngress", ready.Ingress.Pds, "requesterIngress", ready.Ingress.Requester)
}
r.last = ready
return nil
}
func readRequesterReady(a *app, vals clicfg.Values, rest []string, verb string) (request.Ready, string, string, int) {
pos, pass := splitWords(rest)
if len(pos) != 1 || len(pass) > 0 {
return request.Ready{}, "", "", a.fail(exitRefused, "run requester %s <name>: one requester name", verb)
}
stateRoot, dir, err := requesterHome(a, vals, pos[0])
if err != nil {
return request.Ready{}, "", "", a.fail(exitRefused, "run requester %s: %v", verb, err)
}
path, err := requesterReadyPath(vals, dir)
if err != nil {
return request.Ready{}, "", "", a.fail(exitRefused, "run requester %s: --ready-file: %v", verb, err)
}
ready, err := request.ReadReady(path)
if err != nil {
return ready, "", "", a.fail(exitRefused, "run requester %s: %v", verb, err)
}
if ready.Name != pos[0] {
return ready, "", "", a.fail(exitRefused, "run requester %s: the ready file %s is the requester %s's, not %s", verb, path, ready.Name, pos[0])
}
return ready, stateRoot, dir, exitOK
}
func runRequesterEnv(ctx context.Context, a *app, vals clicfg.Values, rest []string) int {
ready, _, dir, code := readRequesterReady(a, vals, rest, "env")
if code != exitOK {
return code
}
ex := []devinstance.Export{
{Name: "KUBECONFIG", Value: ready.Kubeconfig},
{Name: "SOCIALWEB_KCP_HOST", Value: ready.KcpURL},
{Name: "SOCIALWEB_KCP_WORKSPACE", Value: ready.Workspace},
{Name: "SOCIALWEB_REQUESTER_NAME", Value: ready.Name},
{Name: "SOCIALWEB_REQUESTER_DID", Value: ready.Did},
{Name: "SOCIALWEB_REQUESTER_DIR", Value: dir},
{Name: "SOCIALWEB_REQUESTER_SSH_PUBLIC_KEY", Value: ready.SSHPublicKey},
}
if vals.Bool("json") {
a.printJSON(exportMap(ex))
return exitOK
}
fmt.Fprint(a.stdout, devinstance.ShellExports(ex))
return exitOK
}
func (a *app) openRequester(ctx context.Context, vals clicfg.Values, rest []string, verb string) (*requestRun, int) {
ready, stateRoot, dir, code := readRequesterReady(a, vals, rest, verb)
if code != exitOK {
return nil, code
}
r, err := newRequestRun(a, vals)
if err != nil {
return nil, a.fail(exitRefused, "run requester %s: %v", verb, err)
}
r.stateRoot, r.homeDir, r.verb = stateRoot, dir, requesterVerb
r.inv = request.Invocation{Name: ready.Name, StateRoot: stateRoot}
k, err := request.OpenKcp(ready.Kubeconfig, ready.Workspace)
if err != nil {
return nil, a.fail(exitFailed, "run requester %s: %v", verb, err)
}
actx, cancel := context.WithTimeout(ctx, requestAttachProbe)
defer cancel()
st, err := k.Root(actx)
if err != nil {
return nil, a.fail(exitFailed, "run requester %s: the requester's kcp does not answer (is run requester %s running?): %v", verb, ready.Name, err)
}
if st.Phase != v1alpha1.ApplicationPhaseReady {
return nil, a.fail(exitFailed, "run requester %s: the MarketRequester is %s: %s", verb, st.Phase, st.Message)
}
if r.target == "" {
return nil, a.fail(exitRefused, "run requester %s: --contract names the ComputeContract (a run requester has no contract of its own)", verb)
}
r.kcp = k.ForContract(r.target)
return r, exitOK
}
func runRequesterSSH(ctx context.Context, a *app, vals clicfg.Values, rest []string) int {
r, code := a.openRequester(ctx, vals, rest, "ssh")
if r == nil {
return code
}
if _, err := exec.LookPath("ssh"); err != nil {
return a.fail(exitRefused, "run requester ssh: ssh is not on PATH; install the OpenSSH client (apt install openssh-client, dnf install openssh-clients)")
}
c, err := r.kcp.WaitContract(ctx, r.contractTimeout, contractSettled, r.progress)
if err != nil {
return a.fail(exitFailed, "run requester ssh: %v", err)
}
if c.Status.Phase != v1alpha1.ComputeContractHeld {
return a.fail(exitFailed, "run requester ssh: the contract is %s, not Held: %s", c.Status.Phase, c.Status.Message)
}
ec, err := r.ssh(ctx, c)
if err != nil {
return a.fail(exitFailed, "run requester ssh: %v", err)
}
if ec != 0 {
return exitFailed
}
return exitOK
}
func runRequesterProxy(ctx context.Context, a *app, vals clicfg.Values, rest []string) int {
return proxyRequest(ctx, a, vals, requesterVerb+" proxy", func(q *app) (*requestRun, bool, int) {
r, code := q.openRequester(ctx, vals, rest, "proxy")
return r, false, code
})
}