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 gatewaymain
import (
"fmt"
"log/slog"
"net"
"net/url"
"strconv"
"strings"
"time"
"github.com/johnandersen777/socialweb-computer-kcp/internal/kcpclient"
"github.com/johnandersen777/socialweb-computer-kcp/internal/kit/logx"
)
const (
envAddr = "SOCIALWEB_GATEWAY_ADDR"
envAudiences = "SOCIALWEB_GATEWAY_AUDIENCES"
envKCPHost = "SOCIALWEB_KCP_HOST"
envKCPKubeconfig = "SOCIALWEB_KCP_KUBECONFIG"
envDirectoryTTL = "SOCIALWEB_GATEWAY_DIRECTORY_CACHE_TTL"
envShutdownTimeout = "SOCIALWEB_GATEWAY_SHUTDOWN_TIMEOUT"
envLogLevel = "SOCIALWEB_GATEWAY_LOG_LEVEL"
envPLCURL = "SOCIALWEB_GATEWAY_PLC_URL"
envPLCTimeout = "SOCIALWEB_GATEWAY_PLC_TIMEOUT"
envKCPPublicHost = "SOCIALWEB_GATEWAY_KCP_PUBLIC_HOST"
envKCPPublicCAFile = "SOCIALWEB_GATEWAY_KCP_PUBLIC_CA_FILE"
envClusterStateDir = "SOCIALWEB_GATEWAY_CLUSTER_STATE_DIR"
envNodeClaimAgent = "SOCIALWEB_GATEWAY_NODECLAIM_AGENT"
envNodeClaimQuota = "SOCIALWEB_GATEWAY_NODECLAIM_QUOTA"
envHostProcessWorkspace = "SOCIALWEB_CLUSTERPROVIDER_HOSTPROCESS_WORKSPACE"
)
const DefaultDirectoryCacheTTL = 60 * time.Second
const DefaultPLCTimeout = 10 * time.Second
const DefaultShutdownTimeout = 30 * time.Second
type Config struct {
Addr string
Audiences []string
KCPHost string
KCPKubeconfig string
DirectoryCacheTTL time.Duration
PLCURL string
PLCTimeout time.Duration
ShutdownTimeout time.Duration
KCPPublicHost string
KCPPublicCAFile string
ClusterStateDir string
NodeClaimAgent *kcpclient.ServiceAccountRef
NodeClaimQuota int64
HostProcessWorkspace string
LogLevel slog.Level
}
func load(get func(string) string) (Config, error) {
cfg := Config{
Addr: strings.TrimSpace(get(envAddr)),
KCPHost: strings.TrimSpace(get(envKCPHost)),
KCPKubeconfig: strings.TrimSpace(get(envKCPKubeconfig)),
PLCURL: strings.TrimSpace(get(envPLCURL)),
KCPPublicCAFile: strings.TrimSpace(get(envKCPPublicCAFile)),
ClusterStateDir: strings.TrimSpace(get(envClusterStateDir)),
DirectoryCacheTTL: DefaultDirectoryCacheTTL,
PLCTimeout: DefaultPLCTimeout,
ShutdownTimeout: DefaultShutdownTimeout,
LogLevel: slog.LevelInfo,
}
for _, req := range []struct {
name string
value string
}{
{envAddr, cfg.Addr},
{envKCPHost, cfg.KCPHost},
{envKCPKubeconfig, cfg.KCPKubeconfig},
{envPLCURL, cfg.PLCURL},
{envClusterStateDir, cfg.ClusterStateDir},
} {
if req.value == "" {
return Config{}, fmt.Errorf("%s is not set", req.name)
}
}
if err := requireLoopbackAddr(cfg.Addr); err != nil {
return Config{}, err
}
cfg.Audiences = splitList(get(envAudiences))
if len(cfg.Audiences) == 0 {
return Config{}, fmt.Errorf("%s is not set: the endpoint would refuse every token as Unauthorized", envAudiences)
}
var err error
if cfg.KCPPublicHost, err = optionalHTTPSURL(get, envKCPPublicHost); err != nil {
return Config{}, err
}
if cfg.NodeClaimAgent, err = kcpclient.ParseServiceAccountRef(get(envNodeClaimAgent)); err != nil {
return Config{}, fmt.Errorf("%s: %w", envNodeClaimAgent, err)
}
cfg.HostProcessWorkspace = strings.TrimSpace(get(envHostProcessWorkspace))
if v := strings.TrimSpace(get(envNodeClaimQuota)); v != "" {
if cfg.NodeClaimQuota, err = strconv.ParseInt(v, 10, 64); err != nil {
return Config{}, fmt.Errorf("%s: %w", envNodeClaimQuota, err)
}
}
if cfg.DirectoryCacheTTL, err = duration(get, envDirectoryTTL, DefaultDirectoryCacheTTL); err != nil {
return Config{}, err
}
if cfg.PLCTimeout, err = duration(get, envPLCTimeout, DefaultPLCTimeout); err != nil {
return Config{}, err
}
if cfg.ShutdownTimeout, err = duration(get, envShutdownTimeout, DefaultShutdownTimeout); err != nil {
return Config{}, err
}
if cfg.LogLevel, err = logx.ParseLevel(get(envLogLevel)); err != nil {
return Config{}, fmt.Errorf("%s: %w", envLogLevel, err)
}
return cfg, nil
}
// ponytail: loopback-only trusts a local proxy to terminate TLS; serve ListenAndServeTLS with
// cert and key paths when this process must terminate TLS itself
func requireLoopbackAddr(addr string) error {
host, _, err := net.SplitHostPort(addr)
if err != nil {
return fmt.Errorf("%s is %q: it is not a host:port address, and this listener must bind "+
"127.0.0.1 or [::1]: %w", envAddr, addr, err)
}
if ip := net.ParseIP(host); ip == nil || !ip.IsLoopback() {
return fmt.Errorf("%s is %q: the host %q is not a loopback address. This listener carries "+
"bearer service-auth tokens in and device credentials with cluster access out, so TLS "+
"must be terminated by a local proxy: bind 127.0.0.1 or [::1] only, never an "+
"all-interfaces address", envAddr, addr, host)
}
return nil
}
func splitList(raw string) []string {
out := []string{}
for part := range strings.SplitSeq(raw, ",") {
if p := strings.TrimSpace(part); p != "" {
out = append(out, p)
}
}
return out
}
func duration(get func(string) string, name string, fallback time.Duration) (time.Duration, error) {
raw := strings.TrimSpace(get(name))
if raw == "" {
return fallback, nil
}
d, err := time.ParseDuration(raw)
if err != nil {
return 0, fmt.Errorf("%s: %q is not a duration", name, raw)
}
if d <= 0 {
return 0, fmt.Errorf("%s: %q must be positive", name, raw)
}
return d, nil
}
func optionalHTTPSURL(get func(string) string, name string) (string, error) {
raw := strings.TrimSpace(get(name))
if raw == "" {
return "", nil
}
u, err := url.Parse(raw)
if err != nil {
return "", fmt.Errorf("%s: %q is not a URL: %w", name, raw, err)
}
if u.Scheme != "https" || u.Host == "" {
return "", fmt.Errorf("%s: %q must be an absolute https URL. This value is rendered "+
"into a kubeconfig a user holds, and client-go silently drops the user identity "+
"block for a server that is not https, so an http one would hand out a document "+
"that authenticates nothing", name, raw)
}
return strings.TrimSuffix(raw, "/"), nil
}