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
package main
import (
"context"
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"path/filepath"
"slices"
"time"
"github.com/fsnotify/fsnotify"
"github.com/johnandersen777/socialweb-computer-kcp/internal/clicfg"
"github.com/johnandersen777/socialweb-computer-kcp/internal/clusterprovider"
"github.com/johnandersen777/socialweb-computer-kcp/internal/kit/fswatch"
"github.com/johnandersen777/socialweb-computer-kcp/internal/nsexec"
)
func runNsExec(ctx context.Context, a *app, vals clicfg.Values, rest []string) int {
binds, err := nsexec.ParseBinds(vals.String("bind"))
if err != nil {
return a.fail(exitRefused, "internal ns-exec: %v", err)
}
c := nsexec.Config{Hostname: vals.String("private-hostname"), Binds: binds, Argv: rest}
if vals.Bool("inside") {
err = nsexec.Inside(c)
} else {
var exe string
if exe, err = os.Executable(); err == nil {
err = nsexec.Enter(c, []string{exe, "internal", "ns-exec"})
}
}
return a.fail(exitFailed, "internal ns-exec: %v", err)
}
func rollID() string {
b := make([]byte, 4)
rand.Read(b)
return time.Now().UTC().Format("20060102T150405Z") + "-" + hex.EncodeToString(b)
}
func withdrawRollRequest(path, id string) {
b, err := os.ReadFile(path)
if err != nil {
return
}
var cur clusterprovider.RollRequest
if json.Unmarshal(b, &cur) == nil && cur.ID == id {
os.Remove(path)
}
}
func rollStalled(state, key string, req clusterprovider.RollRequest) string {
bound := clusterprovider.DefaultRollObserveBound
now := time.Now().UTC()
if o, ok, err := clusterprovider.ReadRollObserved(state, key, req.ID); err == nil && ok {
if o.State == clusterprovider.RollStateWaiting && o.Error != "" && now.Sub(o.FirstAt) > bound {
return fmt.Sprintf("%s took roll %s at %s but cannot read the cluster's readiness for %s: %s",
o.Provider, req.ID, o.FirstAt.Format(time.RFC3339), now.Sub(o.FirstAt).Round(time.Second), o.Error)
}
return ""
}
if now.Sub(req.RequestedAt) <= bound {
return ""
}
s, ok, err := clusterprovider.ReadRollerStatus(state)
switch {
case err != nil:
return fmt.Sprintf("no cluster provider took roll %s within %s, and its status %s does not read: %v", req.ID, bound, clusterprovider.RollerStatusPath(state), err)
case !ok:
return fmt.Sprintf("no cluster provider took roll %s within %s; none has written %s, so none runs a roller on %s", req.ID, bound, clusterprovider.RollerStatusPath(state), state)
case now.Sub(s.PassAt) > bound:
return fmt.Sprintf("%s did not take roll %s within %s; its last pass was at %s (last error %q at %s)", s.Provider, req.ID, bound,
s.PassAt.Format(time.RFC3339), s.LastError, s.LastErrorAt.Format(time.RFC3339))
case s.Error != "":
return fmt.Sprintf("%s did not take roll %s within %s; its last pass at %s failed: %s", s.Provider, req.ID, bound, s.PassAt.Format(time.RFC3339), s.Error)
case !slices.Contains(s.Clusters, key):
return fmt.Sprintf("%s did not take roll %s within %s; its last pass at %s lists %d clusters and not %s", s.Provider, req.ID, bound, s.PassAt.Format(time.RFC3339), len(s.Clusters), key)
}
return fmt.Sprintf("%s did not take roll %s within %s; its last pass at %s reported no error (last error %q at %s)", s.Provider, req.ID, bound,
s.PassAt.Format(time.RFC3339), s.LastError, s.LastErrorAt.Format(time.RFC3339))
}
func runUnitsRoll(kind string) verbFunc {
verb := "units roll " + kind
return func(ctx context.Context, a *app, vals clicfg.Values, rest []string) int {
if len(rest) != 1 {
return a.fail(exitRefused, "%s: name exactly one cluster key (the <account>-<cluster> the provider knows it by), got %v", verb, rest)
}
key := rest[0]
state := vals.String("cluster-state-dir")
if state == "" || !filepath.IsAbs(state) {
return a.fail(exitRefused, "%s: --cluster-state-dir must name the cluster provider's absolute state dir", verb)
}
wait, err := time.ParseDuration(vals.String("ready-timeout"))
if err != nil {
return a.fail(exitRefused, "%s: ready-timeout: %v", verb, err)
}
req := clusterprovider.RollRequest{ID: rollID(), Kind: kind, RequestedAt: time.Now().UTC()}
switch kind {
case clusterprovider.RollKindCluster:
req.K3s, req.Kine, req.IdleBound = vals.String("k3s-path"), vals.String("kine-path"), vals.String("idle-bound")
if _, err := time.ParseDuration(req.IdleBound); err != nil {
return a.fail(exitRefused, "%s: idle-bound: %v", verb, err)
}
case clusterprovider.RollKindLink:
req.Service, req.Dumbpipe = vals.String("service"), vals.String("dumbpipe-path")
if req.Service == "" {
return a.fail(exitRefused, "%s: --service names the link to replace (apiserver or openbao)", verb)
}
}
for _, p := range []string{req.K3s, req.Kine, req.Dumbpipe} {
if p != "" && !filepath.IsAbs(p) {
return a.fail(exitRefused, "%s: %s must be an absolute path", verb, p)
}
}
path, err := clusterprovider.WriteRollRequest(state, key, req)
if err != nil {
return a.fail(exitFailed, "%s: %v", verb, err)
}
a.log.Info("roll requested; the cluster provider carries it out", "cluster", key, "roll", req.ID, "request", path)
rolls, err := rollWatch(path, state, key, req.ID)
if err != nil {
return a.fail(exitFailed, "%s: %v", verb, err)
}
defer rolls.Close()
deadline := time.NewTimer(wait)
defer deadline.Stop()
stall := time.NewTimer(rollStallCheck(state, key, req, wait))
defer stall.Stop()
for {
rep, ok, err := clusterprovider.ReadRollReport(state, key, req.ID)
if err != nil {
return a.fail(exitFailed, "%s: %v", verb, err)
}
if ok {
a.printJSON(rep)
if rep.Error != "" {
return exitFailed
}
return exitOK
}
if why := rollStalled(state, key, req); why != "" {
withdrawRollRequest(path, req.ID)
return a.fail(exitFailed, "%s: %s; the request is withdrawn", verb, why)
}
select {
case <-ctx.Done():
return a.fail(exitFailed, "%s: interrupted; the request at %s stays for the provider", verb, path)
case <-deadline.C:
return a.fail(exitFailed, "%s: no report for %s after %s; the request at %s stays for the provider", verb, req.ID, wait, path)
case <-stall.C:
stall.Reset(rollStallCheck(state, key, req, wait))
case ev, open := <-rolls.Events():
if !open {
return a.fail(exitFailed, "%s: the watch of %s closed", verb, path)
}
if ev.Has(fsnotify.Create) && filepath.Base(ev.Name) == clusterprovider.RollsDirName {
_ = rolls.Add(ev.Name)
}
case <-rolls.Errors():
}
}
}
}
func rollWatch(request, state, key, id string) (*fswatch.Sub, error) {
w, err := fswatch.Shared().Subscribe(filepath.Dir(request))
if err != nil {
return nil, err
}
if report, err := clusterprovider.RollReportPath(state, key, id); err == nil {
_ = w.Add(filepath.Dir(report))
}
return w, nil
}
func rollStallCheck(state, key string, req clusterprovider.RollRequest, wait time.Duration) time.Duration {
bound := clusterprovider.DefaultRollObserveBound
since := req.RequestedAt
if o, ok, err := clusterprovider.ReadRollObserved(state, key, req.ID); err == nil && ok {
since = o.FirstAt
}
if next := time.Until(since.Add(bound)); next > 0 {
return next + time.Second
}
return wait
}