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
package clusterprovider
import (
"context"
"errors"
"fmt"
"log/slog"
"github.com/johnandersen777/socialweb-computer-kcp/internal/identitybundle"
"github.com/johnandersen777/socialweb-computer-kcp/internal/instance"
"github.com/johnandersen777/socialweb-computer-kcp/internal/materialiser"
"github.com/johnandersen777/socialweb-computer-kcp/internal/procgroup"
)
var (
ErrNoRowDeleter = errors.New(
"clusterprovider: the datastore holds a bootstrap row encrypted with the previous " +
"instance's token and no row deleter is wired, so k3s would exit immediately; " +
"see internal/instance")
ErrNoBundleSeeder = errors.New(
"clusterprovider: the identity bundle must be pre-seeded before k3s starts and no " +
"seeder is wired; see internal/identitybundle")
ErrNoBundle = errors.New(
"clusterprovider: no identity bundle source was supplied, so the pre-seeded entries " +
"cannot be verified; an unverified bundle is what GuardFor exists to refuse")
ErrNoTokenSource = errors.New(
"clusterprovider: no token source was supplied, so instance.PlanFor cannot answer; the " +
"token plan is not optional because a k3s started with the wrong token dies on the " +
"next start with 'bootstrap data already found and encrypted with different token'")
)
type GuardDeps struct {
Bundle func(cluster string) (*identitybundle.Bundle, error)
StateDir string
Token func(cluster string) instance.Token
HasRow func(ctx context.Context, cluster string) bool
DeleteRow func(ctx context.Context, cluster string) error
SeedBundle func(ctx context.Context, cluster string) error
Log *slog.Logger
}
func GuardFor(d GuardDeps) materialiser.IdentityGuard {
return func(ctx context.Context, o materialiser.Observed) error {
cluster := o.Cluster.Name
log := d.Log
if log == nil {
log = slog.Default()
}
dir, err := ClusterDataDir(d.StateDir, cluster)
if err != nil {
return err
}
if d.Token == nil {
return fmt.Errorf("%w (cluster %s)", ErrNoTokenSource, cluster)
}
if d.Bundle == nil {
return fmt.Errorf("%w (cluster %s)", ErrNoBundle, cluster)
}
if uid := string(o.Cluster.UID); uid != "" {
bdir, err := BundleDir(d.StateDir, cluster)
if err != nil {
return err
}
adopted, err := identitybundle.BindClusterUID(bdir, uid)
if err != nil {
return fmt.Errorf("clusterprovider: %s: refusing to seed or verify it: %w", cluster, err)
}
if adopted {
log.Info("guard: identity bundle adopted for uid", "cluster", cluster, "uid", uid)
}
}
plan, err := instance.PlanFor(d.Token(cluster), d.HasRow != nil && d.HasRow(ctx, cluster))
if err != nil {
return fmt.Errorf("clusterprovider: the token plan for %s: %w", cluster, err)
}
log.Info("guard: token plan", "cluster", cluster, "dataDir", dir,
"deleteBootstrapRow", plan.DeleteBootstrapRow, "reSeedBundle", plan.ReSeedBundle,
"reason", plan.Reason)
if plan.DeleteBootstrapRow {
if d.DeleteRow == nil {
return fmt.Errorf("%w (cluster %s)", ErrNoRowDeleter, cluster)
}
if err := d.DeleteRow(ctx, cluster); err != nil {
return fmt.Errorf("clusterprovider: deleting the bootstrap row for %s: %w", cluster, err)
}
}
if plan.ReSeedBundle {
if d.SeedBundle == nil {
return fmt.Errorf("%w (cluster %s)", ErrNoBundleSeeder, cluster)
}
if err := d.SeedBundle(ctx, cluster); err != nil && !errors.Is(err, ErrNoBundleYet) {
return fmt.Errorf("clusterprovider: seeding the identity bundle for %s: %w", cluster, err)
}
}
b, err := d.Bundle(cluster)
if errors.Is(err, ErrNoBundleYet) {
log.Warn("guard: no identity bundle is persisted for this cluster yet, so there is "+
"nothing to verify. k3s authors the 13 entries on a datastore with no bootstrap row, "+
"and the harvest after the group is up is what persists them; every later start "+
"seeds from that copy and verifies it byte for byte",
"cluster", cluster, "bundleDir", dir)
return nil
}
if err != nil {
return err
}
root, err := bundleRoot(d.StateDir, cluster)
if err != nil {
return err
}
return b.Verify(root)
}
}
var ErrNoHarvestSeam = errors.New(
"clusterprovider: this Stack puts no k3s step in the group, so there is no moment after " +
"the CAs exist and before anything else could need them, and the harvest would SILENTLY " +
"NOT RUN -- a bundle harvested by nothing and a bundle harvested correctly look " +
"identical from a green suite")
func HarvestAfterK3s(steps []procgroup.Step, harvest func(ctx context.Context, cluster string) error, cluster string) ([]procgroup.Step, error) {
if harvest == nil {
return nil, fmt.Errorf(
"clusterprovider: this Stack has a k3s step and no identity-bundle harvest, so the " +
"cluster's CAs would never be persisted and every rebuild would be a NEW cluster " +
"under the same name -- invalidating every kubeconfig issued before it")
}
idx := -1
for i, s := range steps {
if s.Name == procgroup.ComponentK3s {
idx = i
break
}
}
if idx < 0 {
return nil, ErrNoHarvestSeam
}
inner := steps[idx].Ready
steps[idx].Ready = func(ctx context.Context, p procgroup.Member) error {
if inner != nil {
if err := inner(ctx, p); err != nil {
return err
}
}
return harvest(ctx, cluster)
}
return steps, nil
}