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
package vaultcert
import (
"context"
"errors"
"fmt"
"net"
"path/filepath"
"slices"
"strconv"
"time"
"github.com/johnandersen777/socialweb-computer-kcp/internal/clusterstack"
"github.com/johnandersen777/socialweb-computer-kcp/internal/hostos"
"github.com/johnandersen777/socialweb-computer-kcp/internal/kit/opt"
"github.com/johnandersen777/socialweb-computer-kcp/internal/portscan"
)
const (
DefaultVaultReloadSettle = 10 * time.Second
vaultReloadPoll = 100 * time.Millisecond
)
var ErrVaultReloadDidNotTake = errors.New("vaultcert: the running vault was asked to reload its " +
"TLS certificate and still does not present every host asked for")
var ErrNoVaultToReload = errors.New("vaultcert: no process holding the vault's port is this " +
"cluster's vault")
type ServedVaultTLS struct {
DataDir string
Port int
Hosts []string
Reload func(ctx context.Context, dataDir string, port int) error
Settle time.Duration
}
func EnsureServedVaultTLS(ctx context.Context, dataDir string, port int, hosts ...string) (VaultTLS, error) {
return ServedVaultTLS{DataDir: dataDir, Port: port, Hosts: hosts}.Ensure(ctx)
}
func (s ServedVaultTLS) Ensure(ctx context.Context) (VaultTLS, error) {
if s.Port <= 0 || s.Port > 65535 {
return VaultTLS{}, fmt.Errorf("vaultcert: the vault's port %d is not a port, so there is no "+
"running listener to check the hosts %v against", s.Port, s.Hosts)
}
tls, err := EnsureVaultTLSCertificate(s.DataDir, s.Hosts...)
if err != nil {
return VaultTLS{}, err
}
addr := net.JoinHostPort("127.0.0.1", strconv.Itoa(s.Port))
first := RequireServedVaultTLS(ctx, addr, s.Hosts...)
if first == nil {
return tls, nil
}
if !errors.Is(first, ErrVaultServesAnotherLeaf) {
return VaultTLS{}, first
}
reload := s.Reload
if reload == nil {
reload = ReloadVault
}
if err := reload(ctx, s.DataDir, s.Port); err != nil {
return VaultTLS{}, fmt.Errorf("%w. Reloading it was refused: %w", first, err)
}
settle := s.Settle
settle = opt.Positive(settle, DefaultVaultReloadSettle)
deadline := time.Now().Add(settle)
last := first
for time.Now().Before(deadline) {
if err := ctx.Err(); err != nil {
return VaultTLS{}, err
}
last = RequireServedVaultTLS(ctx, addr, s.Hosts...)
if last == nil {
return tls, nil
}
time.Sleep(vaultReloadPoll)
}
return VaultTLS{}, fmt.Errorf("%w within %s: %w. A reload that did not happen and one that "+
"did look identical from the call that sent it, so the served leaf is what is read, and it "+
"still lacks the host: restart the vault, or the cluster that holds it, and run this pass "+
"again", ErrVaultReloadDidNotTake, settle, last)
}
func ReloadVault(_ context.Context, dataDir string, port int) error {
want, err := clusterstack.Spec{OpenBaoConfig: clusterstack.OpenBaoConfigPath(dataDir)}.OpenBaoArgv()
if err != nil {
return err
}
pids, err := portscan.ListenerPIDs(port)
if err != nil {
return fmt.Errorf("vaultcert: finding the process listening on the vault's port %d: %w", port, err)
}
var vault []int
var strangers []string
for _, pid := range pids {
argv, err := hostos.Cmdline(pid)
if err != nil {
strangers = append(strangers, fmt.Sprintf("pid %d (%v)", pid, err))
continue
}
if isVaultArgv(argv, want) {
if !slices.Contains(vault, pid) {
vault = append(vault, pid)
}
continue
}
strangers = append(strangers, fmt.Sprintf("pid %d %v", pid, argv))
}
if len(vault) != 1 {
return fmt.Errorf("%w: port %d is held by %d processes whose command line is %v and by %v. "+
"A process that is not this cluster's vault is never signalled, and two that are cannot "+
"both be this cluster's one listener", ErrNoVaultToReload, port, len(vault), want, strangers)
}
if err := hostos.Reload(vault[0]); err != nil {
return fmt.Errorf("vaultcert: asking the vault at pid %d to reload its certificate: %w", vault[0], err)
}
return nil
}
func isVaultArgv(argv, want []string) bool {
if len(argv) != len(want) || len(argv) == 0 {
return false
}
if filepath.Base(argv[0]) != filepath.Base(want[0]) {
return false
}
return slices.Equal(argv[1:], want[1:])
}