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
package hostlifecycle
import (
"cmp"
"context"
"errors"
"os"
"sort"
"strings"
"time"
"github.com/johnandersen777/socialweb-computer-kcp/api/v1alpha1"
"github.com/johnandersen777/socialweb-computer-kcp/internal/apiserverfront"
)
const (
LinkCurrent = "current"
LinkPrevious = "previous"
)
var ErrNoStateDir = errors.New("hostlifecycle: no host state directory is configured")
var ErrAccount = errors.New("hostlifecycle: the host account names no valid install record")
var ErrNoReader = errors.New("hostlifecycle: the host reader is not configured")
type KcpInstallationObservation struct {
ActiveSlot v1alpha1.KcpSlot
FrontBackend string
Drifted bool
DriftMessage string
LeaseStale bool
LeaseMessage string
LeaseStaleIn time.Duration
}
type HostInstallObservation struct {
Found bool
Account string
InstalledVersion string
Steps []v1alpha1.HostInstallStep
}
type HostReleaseObservation struct {
Current string
Previous []string
SelfCheck *v1alpha1.HostReleaseSelfCheck
GC *v1alpha1.HostReleaseGC
}
type Authority interface {
ObserveKcpInstallation(ctx context.Context, spec v1alpha1.KcpInstallationSpec) (KcpInstallationObservation, error)
ObserveHostInstall(ctx context.Context, spec v1alpha1.HostInstallSpec) (HostInstallObservation, error)
ObserveHostRelease(ctx context.Context, spec v1alpha1.HostReleaseSpec) (HostReleaseObservation, error)
}
type InstallRecord struct {
StateDir string
Steps map[string]string
}
type FileAuthorityOptions struct {
StateDir string
Account string
FrontControl string
Record func(account string) (InstallRecord, error)
ActiveSlot func(context.Context) (string, error)
CoreVersion func(ctx context.Context, stateDir, link string) (string, error)
Drift func(context.Context) (bool, error)
Lease func(context.Context) (time.Time, bool, error)
LeaseStaleAfter time.Duration
SelfCheck func(context.Context, string) *v1alpha1.HostReleaseSelfCheck
GC func(context.Context) *v1alpha1.HostReleaseGC
Now func() time.Time
}
type FileAuthority struct {
o FileAuthorityOptions
}
var _ Authority = (*FileAuthority)(nil)
func NewFileAuthority(o FileAuthorityOptions) *FileAuthority {
return &FileAuthority{o: o}
}
func (a *FileAuthority) now() time.Time {
if a.o.Now != nil {
return a.o.Now()
}
return time.Now()
}
func (a *FileAuthority) ObserveKcpInstallation(ctx context.Context, _ v1alpha1.KcpInstallationSpec) (KcpInstallationObservation, error) {
var out KcpInstallationObservation
if a.o.ActiveSlot == nil {
return out, ErrNoReader
}
active, err := a.o.ActiveSlot(ctx)
if err != nil {
return out, err
}
out.ActiveSlot = v1alpha1.KcpSlot(active)
if a.o.FrontControl != "" {
st, err := (apiserverfront.Client{Path: a.o.FrontControl}).Status(ctx)
if err != nil {
return out, err
}
out.FrontBackend = st.Backend
}
if a.o.Drift != nil {
drifted, err := a.o.Drift(ctx)
if err != nil {
return out, err
}
if drifted {
out.Drifted = true
out.DriftMessage = "the active slot process differs from its unit"
}
}
if a.o.Lease != nil && a.o.LeaseStaleAfter > 0 {
renewed, found, err := a.o.Lease(ctx)
if err != nil {
return out, err
}
if found && !renewed.IsZero() {
left := a.o.LeaseStaleAfter - a.now().Sub(renewed)
if left < 0 {
out.LeaseStale = true
out.LeaseMessage = "the controller lease has not renewed for over " + a.o.LeaseStaleAfter.String()
} else {
out.LeaseStaleIn = left + time.Millisecond
}
}
}
return out, nil
}
func (a *FileAuthority) ObserveHostInstall(ctx context.Context, spec v1alpha1.HostInstallSpec) (HostInstallObservation, error) {
var out HostInstallObservation
account := cmp.Or(spec.Account, a.o.Account)
if !validAccount(account) {
return out, ErrAccount
}
out.Account = account
if a.o.Record == nil {
return out, ErrNoReader
}
rec, err := a.o.Record(account)
if errors.Is(err, os.ErrNotExist) {
return out, nil
}
if err != nil {
return out, err
}
out.Found = true
names := make([]string, 0, len(rec.Steps))
for name := range rec.Steps {
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
out.Steps = append(out.Steps, v1alpha1.HostInstallStep{Name: name, Phase: rec.Steps[name]})
}
stateDir := cmp.Or(rec.StateDir, a.o.StateDir)
if stateDir != "" && a.o.CoreVersion != nil {
if v, err := a.o.CoreVersion(ctx, stateDir, LinkCurrent); err == nil {
out.InstalledVersion = v
}
}
return out, nil
}
func (a *FileAuthority) ObserveHostRelease(ctx context.Context, _ v1alpha1.HostReleaseSpec) (HostReleaseObservation, error) {
var out HostReleaseObservation
if a.o.StateDir == "" {
return out, ErrNoStateDir
}
if a.o.CoreVersion == nil {
return out, ErrNoReader
}
cur, err := a.o.CoreVersion(ctx, a.o.StateDir, LinkCurrent)
if err != nil {
return out, err
}
out.Current = cur
if prev, err := a.o.CoreVersion(ctx, a.o.StateDir, LinkPrevious); err == nil && prev != "" && prev != out.Current {
out.Previous = []string{prev}
}
if a.o.SelfCheck != nil {
out.SelfCheck = a.o.SelfCheck(ctx, out.Current)
}
if a.o.GC != nil {
out.GC = a.o.GC(ctx)
}
return out, nil
}
func validAccount(account string) bool {
return account != "" && account != "." && account != ".." && !strings.ContainsAny(account, `/\`)
}