Skip to content

Commit 7ce1330

Browse files
committed
/internal/storage/dbproxy/server: wip, external dolt server
1 parent e7dbd61 commit 7ce1330

5 files changed

Lines changed: 546 additions & 3 deletions

File tree

internal/configfile/configfile_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"path/filepath"
66
"strings"
77
"testing"
8+
"time"
89

910
"github.com/steveyegge/beads/internal/config"
1011
)
@@ -528,6 +529,75 @@ func TestProxiedServerClientInfo_RoundTrip(t *testing.T) {
528529
t.Errorf("got %+v, want %+v", got, want)
529530
}
530531
})
532+
533+
t.Run("external section survives save/load", func(t *testing.T) {
534+
sub := t.TempDir()
535+
want := &ProxiedServerClientInfo{
536+
External: &ExternalDoltConfig{
537+
Host: "db.internal",
538+
Port: 3306,
539+
TLSRequired: true,
540+
TLSCert: "/etc/beads/client.pem",
541+
TLSKey: "/etc/beads/client.key",
542+
KeepAlivePeriod: 45 * time.Second,
543+
},
544+
}
545+
if err := SaveProxiedServerClientInfo(sub, want); err != nil {
546+
t.Fatalf("Save: %v", err)
547+
}
548+
got, err := LoadProxiedServerClientInfo(sub)
549+
if err != nil || got == nil {
550+
t.Fatalf("Load: %v got=%v", err, got)
551+
}
552+
if got.RootPath != "" || got.ConfigPath != "" || got.LogPath != "" {
553+
t.Errorf("local fields leaked into external-only sidecar: %+v", got)
554+
}
555+
if got.External == nil {
556+
t.Fatalf("External section dropped on round-trip")
557+
}
558+
if *got.External != *want.External {
559+
t.Errorf("got %+v, want %+v", got.External, want.External)
560+
}
561+
})
562+
563+
t.Run("local fields and external section coexist", func(t *testing.T) {
564+
sub := t.TempDir()
565+
want := &ProxiedServerClientInfo{
566+
RootPath: "/var/lib/beads/proxieddb",
567+
External: &ExternalDoltConfig{Socket: "/var/run/dolt.sock"},
568+
}
569+
if err := SaveProxiedServerClientInfo(sub, want); err != nil {
570+
t.Fatalf("Save: %v", err)
571+
}
572+
got, err := LoadProxiedServerClientInfo(sub)
573+
if err != nil || got == nil {
574+
t.Fatalf("Load: %v got=%v", err, got)
575+
}
576+
if got.RootPath != want.RootPath {
577+
t.Errorf("RootPath = %q, want %q", got.RootPath, want.RootPath)
578+
}
579+
if got.External == nil || got.External.Socket != "/var/run/dolt.sock" {
580+
t.Errorf("External round-trip lost data: %+v", got.External)
581+
}
582+
})
583+
584+
t.Run("legacy sidecar without external section still loads", func(t *testing.T) {
585+
sub := t.TempDir()
586+
legacy := []byte(`{"root_path":"/var/lib/beads/proxieddb","config_path":"/etc/dolt/server.yaml","log_path":"/var/log/beads/server.log"}`)
587+
if err := os.WriteFile(ProxiedServerClientInfoPath(sub), legacy, 0o600); err != nil {
588+
t.Fatalf("seed legacy: %v", err)
589+
}
590+
got, err := LoadProxiedServerClientInfo(sub)
591+
if err != nil || got == nil {
592+
t.Fatalf("Load: %v got=%v", err, got)
593+
}
594+
if got.External != nil {
595+
t.Errorf("External should be nil for legacy sidecar, got %+v", got.External)
596+
}
597+
if got.RootPath != "/var/lib/beads/proxieddb" {
598+
t.Errorf("RootPath = %q, want /var/lib/beads/proxieddb", got.RootPath)
599+
}
600+
})
531601
}
532602

533603
func TestProxiedServerClientInfo_ResolvedPaths(t *testing.T) {
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package configfile
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"path/filepath"
7+
"time"
8+
)
9+
10+
type ExternalDoltConfig struct {
11+
Host string `json:"host,omitempty"`
12+
Port int `json:"port,omitempty"`
13+
Socket string `json:"socket,omitempty"`
14+
TLSRequired bool `json:"tls_required,omitempty"`
15+
TLSCert string `json:"tls_cert,omitempty"`
16+
TLSKey string `json:"tls_key,omitempty"`
17+
KeepAlivePeriod time.Duration `json:"keep_alive_period,omitempty"`
18+
}
19+
20+
func (c ExternalDoltConfig) Validate() error {
21+
hasHost := c.Host != ""
22+
hasPort := c.Port != 0
23+
hasSocket := c.Socket != ""
24+
25+
switch {
26+
case hasSocket && (hasHost || hasPort):
27+
return errors.New("ExternalDoltConfig: set either Socket OR (Host, Port), not both")
28+
case !hasSocket && !hasHost && !hasPort:
29+
return errors.New("ExternalDoltConfig: must set Socket or (Host, Port)")
30+
case hasHost && !hasPort:
31+
return errors.New("ExternalDoltConfig: Host requires Port")
32+
case !hasHost && hasPort:
33+
return errors.New("ExternalDoltConfig: Port requires Host")
34+
}
35+
36+
if hasHost && (c.Port < 1 || c.Port > 65535) {
37+
return fmt.Errorf("ExternalDoltConfig: Port %d out of range [1, 65535]", c.Port)
38+
}
39+
40+
if hasSocket && !filepath.IsAbs(c.Socket) {
41+
return fmt.Errorf("ExternalDoltConfig: Socket %q is not absolute", c.Socket)
42+
}
43+
44+
switch {
45+
case c.TLSCert != "" && c.TLSKey == "":
46+
return errors.New("ExternalDoltConfig: TLSCert set without TLSKey")
47+
case c.TLSCert == "" && c.TLSKey != "":
48+
return errors.New("ExternalDoltConfig: TLSKey set without TLSCert")
49+
}
50+
51+
if c.TLSCert != "" && !filepath.IsAbs(c.TLSCert) {
52+
return fmt.Errorf("ExternalDoltConfig: TLSCert %q is not absolute", c.TLSCert)
53+
}
54+
if c.TLSKey != "" && !filepath.IsAbs(c.TLSKey) {
55+
return fmt.Errorf("ExternalDoltConfig: TLSKey %q is not absolute", c.TLSKey)
56+
}
57+
58+
if c.KeepAlivePeriod < 0 {
59+
return fmt.Errorf("ExternalDoltConfig: KeepAlivePeriod %s is negative", c.KeepAlivePeriod)
60+
}
61+
62+
return nil
63+
}

internal/configfile/proxied_server_client_info.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ import (
1010
const ProxiedServerClientInfoFileName = "proxied_server_client_info.json"
1111

1212
type ProxiedServerClientInfo struct {
13-
RootPath string `json:"root_path,omitempty"`
14-
ConfigPath string `json:"config_path,omitempty"`
15-
LogPath string `json:"log_path,omitempty"`
13+
RootPath string `json:"root_path,omitempty"`
14+
ConfigPath string `json:"config_path,omitempty"`
15+
LogPath string `json:"log_path,omitempty"`
16+
External *ExternalDoltConfig `json:"external,omitempty"`
1617
}
1718

1819
func ProxiedServerClientInfoPath(beadsDir string) string {
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package server
2+
3+
import (
4+
"context"
5+
"crypto/sha256"
6+
"encoding/hex"
7+
"errors"
8+
"fmt"
9+
"net"
10+
"strconv"
11+
"sync/atomic"
12+
"time"
13+
14+
"github.com/steveyegge/beads/internal/configfile"
15+
"github.com/steveyegge/beads/internal/storage/dbproxy/util"
16+
)
17+
18+
type ExternalDoltServer struct {
19+
id string
20+
host string
21+
port int
22+
socket string
23+
tlsRequired bool
24+
tlsCert string
25+
tlsKey string
26+
keepAlivePeriod time.Duration
27+
28+
started atomic.Bool
29+
}
30+
31+
var _ DatabaseServer = (*ExternalDoltServer)(nil)
32+
33+
func NewExternalDoltServer(cfg configfile.ExternalDoltConfig) (*ExternalDoltServer, error) {
34+
if err := cfg.Validate(); err != nil {
35+
return nil, err
36+
}
37+
keepAlive := cfg.KeepAlivePeriod
38+
if keepAlive == 0 {
39+
keepAlive = defaultKeepAlivePeriod
40+
}
41+
sum := sha256.Sum256([]byte(externalDoltServerTarget(cfg)))
42+
return &ExternalDoltServer{
43+
id: hex.EncodeToString(sum[:]),
44+
host: cfg.Host,
45+
port: cfg.Port,
46+
socket: cfg.Socket,
47+
tlsRequired: cfg.TLSRequired,
48+
tlsCert: cfg.TLSCert,
49+
tlsKey: cfg.TLSKey,
50+
keepAlivePeriod: keepAlive,
51+
}, nil
52+
}
53+
54+
func externalDoltServerTarget(cfg configfile.ExternalDoltConfig) string {
55+
if cfg.Socket != "" {
56+
return "unix://" + cfg.Socket
57+
}
58+
return "tcp://" + net.JoinHostPort(cfg.Host, strconv.Itoa(cfg.Port))
59+
}
60+
61+
func (s *ExternalDoltServer) ID(_ context.Context) string {
62+
return s.id
63+
}
64+
65+
func (s *ExternalDoltServer) DSN(_ context.Context, database, user, password string) string {
66+
dsn := util.DoltServerDSN{
67+
User: user,
68+
Password: password,
69+
Database: database,
70+
TLSRequired: s.tlsRequired,
71+
TLSCert: s.tlsCert,
72+
TLSKey: s.tlsKey,
73+
}
74+
if s.socket != "" {
75+
dsn.Socket = s.socket
76+
} else {
77+
dsn.Host = s.host
78+
dsn.Port = s.port
79+
}
80+
return dsn.String()
81+
}
82+
83+
func (s *ExternalDoltServer) Start(_ context.Context) error {
84+
if !s.started.CompareAndSwap(false, true) {
85+
return errors.New("server: ExternalDoltServer.Start: server already started")
86+
}
87+
return nil
88+
}
89+
90+
func (s *ExternalDoltServer) Stop(_ context.Context) error {
91+
s.started.Store(false)
92+
return nil
93+
}
94+
95+
func (s *ExternalDoltServer) Running(_ context.Context) bool {
96+
return s.started.Load()
97+
}
98+
99+
func (s *ExternalDoltServer) Dial(ctx context.Context) (net.Conn, error) {
100+
network, addr := "tcp", net.JoinHostPort(s.host, strconv.Itoa(s.port))
101+
if s.socket != "" {
102+
network, addr = "unix", s.socket
103+
}
104+
var d net.Dialer
105+
conn, err := d.DialContext(ctx, network, addr)
106+
if err != nil {
107+
return nil, fmt.Errorf("server: ExternalDoltServer.Dial: %w", err)
108+
}
109+
if tc, ok := conn.(*net.TCPConn); ok {
110+
_ = tc.SetKeepAlive(true)
111+
_ = tc.SetKeepAlivePeriod(s.keepAlivePeriod)
112+
}
113+
return conn, nil
114+
}

0 commit comments

Comments
 (0)