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
package kcpserver
import (
"errors"
"fmt"
"io/fs"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"time"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
)
const (
MemberTokenFile = "members.token"
KcpAdminUser = "kcp-admin"
)
func MemberTokenPath(dir string) string { return filepath.Join(dir, MemberTokenFile) }
func SlotAdminToken(slotRoot string) (string, error) {
cfg, err := clientcmd.LoadFromFile(filepath.Join(slotRoot, "admin.kubeconfig"))
if err != nil {
return "", err
}
u, ok := cfg.AuthInfos[KcpAdminUser]
if !ok || u.Token == "" {
return "", fmt.Errorf("kcpserver: %s/admin.kubeconfig holds no %s token", slotRoot, KcpAdminUser)
}
return u.Token, nil
}
func RefreshMemberToken(slotRoot, tokenFile string) (bool, error) {
token, err := SlotAdminToken(slotRoot)
if err != nil {
return false, err
}
if old, err := os.ReadFile(tokenFile); err == nil && strings.TrimSpace(string(old)) == token {
return false, nil
}
return true, writeTokenAtomic(tokenFile, []byte(token+"\n"))
}
func RefreshMemberTokenIfPresent(slotRoot, tokenFile string) (bool, error) {
if _, err := os.Stat(tokenFile); errors.Is(err, fs.ErrNotExist) {
return false, nil
}
return RefreshMemberToken(slotRoot, tokenFile)
}
func writeTokenAtomic(path string, data []byte) error {
f, err := os.CreateTemp(filepath.Dir(path), "."+filepath.Base(path)+".*")
if err != nil {
return err
}
tmp := f.Name()
if _, err := f.Write(data); err != nil {
f.Close()
os.Remove(tmp)
return err
}
if err := f.Close(); err != nil {
os.Remove(tmp)
return err
}
if err := os.Chmod(tmp, 0o600); err != nil {
os.Remove(tmp)
return err
}
return os.Rename(tmp, path)
}
type tokenFileTransport struct {
path string
base http.RoundTripper
mu sync.Mutex
stamp time.Time
size int64
token string
}
func (t *tokenFileTransport) current() string {
st, err := os.Stat(t.path)
t.mu.Lock()
defer t.mu.Unlock()
if err != nil {
return t.token
}
if t.token != "" && st.ModTime().Equal(t.stamp) && st.Size() == t.size {
return t.token
}
b, err := os.ReadFile(t.path)
if err != nil {
return t.token
}
t.token, t.stamp, t.size = strings.TrimSpace(string(b)), st.ModTime(), st.Size()
return t.token
}
func (t *tokenFileTransport) RoundTrip(req *http.Request) (*http.Response, error) {
if req.Header.Get("Authorization") != "" {
return t.base.RoundTrip(req)
}
token := t.current()
if token == "" {
return t.base.RoundTrip(req)
}
req = req.Clone(req.Context())
req.Header.Set("Authorization", "Bearer "+token)
return t.base.RoundTrip(req)
}
func FollowTokenFile(cfg *rest.Config) *rest.Config {
if cfg == nil || cfg.BearerTokenFile == "" {
return cfg
}
path := cfg.BearerTokenFile
cfg.BearerToken, cfg.BearerTokenFile = "", ""
cfg.Wrap(func(rt http.RoundTripper) http.RoundTripper { return &tokenFileTransport{path: path, base: rt} })
return cfg
}